package restapi import ( "fmt" "net/http" "net/http/httputil" "net/url" "go.uber.org/zap" ) // NewBackendConsoleProxy builds the reverse proxy that forwards operator // console traffic (`/_gm` and `/_gm/*`) to the backend at backendBaseURL. // // The proxy is intentionally thin: it preserves the inbound request path and // the inbound Host header — the latter so the backend's same-origin CSRF check // observes the public host rather than the internal upstream — and relays the // backend response unchanged, including its 401 Basic Auth challenge. It // answers 502 when the backend is unreachable. Authentication, rendering, and // every state change live in the backend; the gateway contributes only the // public anti-abuse layer that runs ahead of this handler. func NewBackendConsoleProxy(backendBaseURL string, logger *zap.Logger) (http.Handler, error) { target, err := url.Parse(backendBaseURL) if err != nil { return nil, fmt.Errorf("parse backend base URL %q: %w", backendBaseURL, err) } if target.Scheme == "" || target.Host == "" { return nil, fmt.Errorf("backend base URL %q must be absolute", backendBaseURL) } if logger == nil { logger = zap.NewNop() } logger = logger.Named("admin_console_proxy") return &httputil.ReverseProxy{ Rewrite: func(pr *httputil.ProxyRequest) { pr.SetURL(target) // SetURL clears Out.Host so the target host is used; restore the // inbound Host so the backend sees the public origin. pr.Out.Host = pr.In.Host }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { logger.Warn("admin console upstream error", zap.String("path", r.URL.Path), zap.Error(err)) w.WriteHeader(http.StatusBadGateway) }, }, nil }