2026-04-01 20:08:55 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
2026-03-14 10:03:29 +00:00
|
|
|
|
|
|
|
|
package provider
|
|
|
|
|
|
2026-03-14 12:22:27 +00:00
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httputil"
|
|
|
|
|
"net/url"
|
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.
Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
|
|
|
|
|
|
|
|
core "dappco.re/go/core"
|
2026-03-14 12:22:27 +00:00
|
|
|
|
2026-04-01 21:42:04 +00:00
|
|
|
coreapi "dappco.re/go/core/api"
|
2026-03-14 12:22:27 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ProxyConfig configures a ProxyProvider that reverse-proxies to an upstream
|
|
|
|
|
// process (typically a runtime provider binary listening on 127.0.0.1).
|
|
|
|
|
type ProxyConfig struct {
|
|
|
|
|
// Name is the provider identity, e.g. "cool-widget".
|
|
|
|
|
Name string
|
|
|
|
|
|
|
|
|
|
// BasePath is the API route prefix, e.g. "/api/v1/cool-widget".
|
|
|
|
|
BasePath string
|
|
|
|
|
|
|
|
|
|
// Upstream is the full URL of the upstream process,
|
|
|
|
|
// e.g. "http://127.0.0.1:9901".
|
|
|
|
|
Upstream string
|
|
|
|
|
|
|
|
|
|
// Element describes the custom element for GUI rendering.
|
|
|
|
|
// Leave zero-value if the provider has no UI.
|
|
|
|
|
Element ElementSpec
|
|
|
|
|
|
|
|
|
|
// SpecFile is the filesystem path to the provider's OpenAPI spec.
|
|
|
|
|
// Used by the Swagger aggregator. Leave empty if none.
|
|
|
|
|
SpecFile string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProxyProvider reverse-proxies requests to an upstream HTTP process.
|
|
|
|
|
// It implements Provider and Renderable so it integrates with the
|
|
|
|
|
// service provider framework and GUI discovery.
|
|
|
|
|
type ProxyProvider struct {
|
|
|
|
|
config ProxyConfig
|
|
|
|
|
proxy *httputil.ReverseProxy
|
2026-04-01 21:42:04 +00:00
|
|
|
err error
|
2026-03-14 12:22:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewProxy creates a ProxyProvider from the given configuration.
|
2026-04-01 21:42:04 +00:00
|
|
|
// Invalid upstream URLs do not panic; the provider retains the
|
|
|
|
|
// configuration error and responds with a standard 500 envelope when
|
|
|
|
|
// mounted. This keeps provider construction safe for callers.
|
2026-03-14 12:22:27 +00:00
|
|
|
func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
|
|
|
|
target, err := url.Parse(cfg.Upstream)
|
|
|
|
|
if err != nil {
|
2026-04-01 21:42:04 +00:00
|
|
|
return &ProxyProvider{
|
|
|
|
|
config: cfg,
|
|
|
|
|
err: err,
|
|
|
|
|
}
|
2026-03-14 12:22:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 08:38:41 +01:00
|
|
|
// url.Parse accepts inputs like "127.0.0.1:9901" without error — they
|
|
|
|
|
// parse without a scheme or host, which causes httputil.ReverseProxy to
|
|
|
|
|
// fail silently at runtime. Require both to be present.
|
|
|
|
|
if target.Scheme == "" || target.Host == "" {
|
|
|
|
|
return &ProxyProvider{
|
|
|
|
|
config: cfg,
|
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.
Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
|
|
|
err: core.E("ProxyProvider.New", core.Sprintf("upstream %q must include a scheme and host (e.g. http://127.0.0.1:9901)", cfg.Upstream), nil),
|
2026-04-07 08:38:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 12:22:27 +00:00
|
|
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
|
|
|
|
|
|
|
|
// Preserve the original Director but strip the base path so the
|
|
|
|
|
// upstream receives clean paths (e.g. /items instead of /api/v1/cool-widget/items).
|
|
|
|
|
defaultDirector := proxy.Director
|
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.
Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
|
|
|
basePath := core.TrimSuffix(cfg.BasePath, "/")
|
2026-03-14 12:22:27 +00:00
|
|
|
|
|
|
|
|
proxy.Director = func(req *http.Request) {
|
|
|
|
|
defaultDirector(req)
|
|
|
|
|
// Strip the base path prefix from the request path.
|
2026-04-01 20:08:55 +00:00
|
|
|
req.URL.Path = stripBasePath(req.URL.Path, basePath)
|
|
|
|
|
if req.URL.RawPath != "" {
|
|
|
|
|
req.URL.RawPath = stripBasePath(req.URL.RawPath, basePath)
|
2026-03-14 12:22:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &ProxyProvider{
|
|
|
|
|
config: cfg,
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 21:42:04 +00:00
|
|
|
// Err reports any configuration error detected while constructing the proxy.
|
|
|
|
|
// A nil error means the proxy is ready to mount and serve requests.
|
|
|
|
|
func (p *ProxyProvider) Err() error {
|
|
|
|
|
if p == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return p.err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 20:08:55 +00:00
|
|
|
// stripBasePath removes an exact base path prefix from a request path.
|
|
|
|
|
// It only strips when the path matches the base path itself or lives under
|
|
|
|
|
// the base path boundary, so "/api" will not accidentally trim "/api-v2".
|
|
|
|
|
func stripBasePath(path, basePath string) string {
|
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.
Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
|
|
|
basePath = core.TrimSuffix(core.Trim(basePath), "/")
|
2026-04-01 20:08:55 +00:00
|
|
|
if basePath == "" || basePath == "/" {
|
|
|
|
|
if path == "" {
|
|
|
|
|
return "/"
|
|
|
|
|
}
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if path == basePath {
|
|
|
|
|
return "/"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prefix := basePath + "/"
|
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.
Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
|
|
|
if core.HasPrefix(path, prefix) {
|
|
|
|
|
trimmed := core.TrimPrefix(path, basePath)
|
2026-04-01 20:08:55 +00:00
|
|
|
if trimmed == "" {
|
|
|
|
|
return "/"
|
|
|
|
|
}
|
|
|
|
|
return trimmed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 12:22:27 +00:00
|
|
|
// Name returns the provider identity.
|
|
|
|
|
func (p *ProxyProvider) Name() string {
|
|
|
|
|
return p.config.Name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BasePath returns the API route prefix.
|
|
|
|
|
func (p *ProxyProvider) BasePath() string {
|
|
|
|
|
return p.config.BasePath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterRoutes mounts a catch-all reverse proxy handler on the router group.
|
|
|
|
|
func (p *ProxyProvider) RegisterRoutes(rg *gin.RouterGroup) {
|
|
|
|
|
rg.Any("/*path", func(c *gin.Context) {
|
2026-04-01 21:42:04 +00:00
|
|
|
if p == nil || p.err != nil || p.proxy == nil {
|
|
|
|
|
details := map[string]any{}
|
|
|
|
|
if p != nil && p.err != nil {
|
|
|
|
|
details["error"] = p.err.Error()
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, coreapi.FailWithDetails(
|
|
|
|
|
"invalid_provider_configuration",
|
|
|
|
|
"Provider is misconfigured",
|
|
|
|
|
details,
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 12:22:27 +00:00
|
|
|
// Use the underlying http.ResponseWriter directly. Gin's
|
|
|
|
|
// responseWriter wrapper does not implement http.CloseNotifier,
|
|
|
|
|
// which httputil.ReverseProxy requires for cancellation signalling.
|
|
|
|
|
var w http.ResponseWriter = c.Writer
|
|
|
|
|
if uw, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
|
|
|
|
|
w = uw.Unwrap()
|
|
|
|
|
}
|
|
|
|
|
p.proxy.ServeHTTP(w, c.Request)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Element returns the custom element specification for GUI rendering.
|
|
|
|
|
func (p *ProxyProvider) Element() ElementSpec {
|
|
|
|
|
return p.config.Element
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SpecFile returns the path to the provider's OpenAPI spec file.
|
|
|
|
|
func (p *ProxyProvider) SpecFile() string {
|
|
|
|
|
return p.config.SpecFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upstream returns the upstream URL string.
|
|
|
|
|
func (p *ProxyProvider) Upstream() string {
|
|
|
|
|
return p.config.Upstream
|
|
|
|
|
}
|