Go: - codegen: pass trimmed specPath to buildArgs instead of raw g.SpecPath - cmd/sdk: use local resolvedSpecFile to avoid mutating flag variable per-invocation - export: write to temp file + atomic rename to prevent destination truncation on failure - openapi: gate effectiveGraphQLPath/SwaggerPath/WSPath/SSEPath on enable flags; use effectiveSwaggerPath in effectiveAuthentikPublicPaths - cache: reject oversized replacement before mutating LRU state for existing keys - ratelimit: move setRateLimitHeaders before c.Next() so headers are sent; hash credential headers with SHA-256 to avoid storing raw secrets; prefer validated principal from context - response_meta: track size separately from body buffer so Size() is accurate after body rewrites and in passthrough mode - bridge: limit request body reads with http.MaxBytesReader (10 MiB); allow missing data key in ValidateResponse for nil/zero success responses; update recorder status in writeErrorResponse - pkg/provider/proxy: validate target scheme and host after url.Parse to catch hostless inputs - cmd_test: snapshot/restore global spec registry in TestAPISpecCmd_Good_RegisteredSpecGroups PHP: - HasApiResponses.php, config.php: add declare(strict_types=1) - RateLimitExceededException: validate Origin against cors.allowed_origins before reflecting in CORS header - ApiUsageService: import and use Core\Api\Models\ApiKey instead of fully-qualified Mod\ path - SeoReportService: add SSRF protection (scheme check, private-IP rejection); add .throw() for HTTP error handling; disable automatic redirects Co-Authored-By: Virgil <virgil@lethean.io>
130 lines
3.6 KiB
Go
130 lines
3.6 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"iter"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
coreio "dappco.re/go/core/io"
|
|
coreerr "dappco.re/go/core/log"
|
|
)
|
|
|
|
// ExportSpec generates the OpenAPI spec and writes it to w.
|
|
// Format must be "json" or "yaml".
|
|
//
|
|
// Example:
|
|
//
|
|
// _ = api.ExportSpec(os.Stdout, "yaml", builder, engine.Groups())
|
|
func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error {
|
|
data, err := builder.Build(groups)
|
|
if err != nil {
|
|
return coreerr.E("ExportSpec", "build spec", err)
|
|
}
|
|
|
|
return writeSpec(w, format, data, "ExportSpec")
|
|
}
|
|
|
|
// ExportSpecIter generates the OpenAPI spec from an iterator and writes it to w.
|
|
// Format must be "json" or "yaml".
|
|
//
|
|
// Example:
|
|
//
|
|
// _ = api.ExportSpecIter(os.Stdout, "json", builder, api.RegisteredSpecGroupsIter())
|
|
func ExportSpecIter(w io.Writer, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) error {
|
|
data, err := builder.BuildIter(groups)
|
|
if err != nil {
|
|
return coreerr.E("ExportSpecIter", "build spec", err)
|
|
}
|
|
|
|
return writeSpec(w, format, data, "ExportSpecIter")
|
|
}
|
|
|
|
func writeSpec(w io.Writer, format string, data []byte, op string) error {
|
|
switch strings.ToLower(strings.TrimSpace(format)) {
|
|
case "json":
|
|
_, err := w.Write(data)
|
|
return err
|
|
case "yaml":
|
|
// Unmarshal JSON then re-marshal as YAML.
|
|
var obj any
|
|
if err := json.Unmarshal(data, &obj); err != nil {
|
|
return coreerr.E(op, "unmarshal spec", err)
|
|
}
|
|
enc := yaml.NewEncoder(w)
|
|
enc.SetIndent(2)
|
|
if err := enc.Encode(obj); err != nil {
|
|
return coreerr.E(op, "encode yaml", err)
|
|
}
|
|
return enc.Close()
|
|
default:
|
|
return coreerr.E(op, fmt.Sprintf("unsupported format %s: use %q or %q", format, "json", "yaml"), nil)
|
|
}
|
|
}
|
|
|
|
// ExportSpecToFile writes the spec to the given path.
|
|
// The parent directory is created if it does not exist.
|
|
//
|
|
// Example:
|
|
//
|
|
// _ = api.ExportSpecToFile("./api/openapi.yaml", "yaml", builder, engine.Groups())
|
|
func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error {
|
|
return exportSpecToFile(path, "ExportSpecToFile", func(w io.Writer) error {
|
|
return ExportSpec(w, format, builder, groups)
|
|
})
|
|
}
|
|
|
|
// ExportSpecToFileIter writes the OpenAPI spec from an iterator to the given path.
|
|
// The parent directory is created if it does not exist.
|
|
//
|
|
// Example:
|
|
//
|
|
// _ = api.ExportSpecToFileIter("./api/openapi.json", "json", builder, api.RegisteredSpecGroupsIter())
|
|
func ExportSpecToFileIter(path, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) error {
|
|
return exportSpecToFile(path, "ExportSpecToFileIter", func(w io.Writer) error {
|
|
return ExportSpecIter(w, format, builder, groups)
|
|
})
|
|
}
|
|
|
|
func exportSpecToFile(path, op string, write func(io.Writer) error) (err error) {
|
|
dir := filepath.Dir(path)
|
|
if err := coreio.Local.EnsureDir(dir); err != nil {
|
|
return coreerr.E(op, "create directory", err)
|
|
}
|
|
|
|
// Write to a temp file in the same directory so the rename is atomic on
|
|
// most filesystems. The destination is never truncated unless the full
|
|
// export succeeds.
|
|
f, err := os.CreateTemp(dir, ".export-*.tmp")
|
|
if err != nil {
|
|
return coreerr.E(op, "create temp file", err)
|
|
}
|
|
tmpPath := f.Name()
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
|
|
if writeErr := write(f); writeErr != nil {
|
|
_ = f.Close()
|
|
return writeErr
|
|
}
|
|
|
|
if closeErr := f.Close(); closeErr != nil {
|
|
return coreerr.E(op, "close temp file", closeErr)
|
|
}
|
|
|
|
if renameErr := os.Rename(tmpPath, path); renameErr != nil {
|
|
return coreerr.E(op, "rename temp file", renameErr)
|
|
}
|
|
return nil
|
|
}
|