package proxy import ( "net/http" "net/http/httputil" "net/url" "path" "strings" ) // NormalizeRequestURLPath collapses duplicate slashes and dot-segments in req.URL.Path // (path.Clean) and clears RawPath so strip-prefix routing matches the client path even // when the request line contained "//" (e.g. /api//mtg/health). Without this, the path // may not match /api/{alias} and SingleHostReverseProxy forwards a wrong path upstream. func NormalizeRequestURLPath(req *http.Request) { if req == nil || req.URL == nil { return } u := req.URL if u.Path == "" { u.Path = "/" u.RawPath = "" return } c := path.Clean(u.Path) if !strings.HasPrefix(c, "/") { c = "/" + c } u.Path = c u.RawPath = "" } // NewReverseProxy builds a reverse proxy to target base URL with path rewriting: // stripPrefix (/api/{alias}) + pathPrefix (/v1) + remainder, joined onto target via url.JoinPath // (e.g. https://host/api/ + v1 + health → https://host/v1/health). // // Compatibility: GET/HEAD .../api/{alias}/users (list only, no extra path segment) is sent upstream as // /v1/stats/users. Some Telemt builds treat GET /v1/users incorrectly (e.g. 400) while /v1/stats/users works. // POST .../users (create) and GET .../users/{username} are unchanged. func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth string) *httputil.ReverseProxy { proxy := httputil.NewSingleHostReverseProxy(target) proxy.Transport = DirectTransport() orig := proxy.Director targetQuery := target.RawQuery proxy.Director = func(req *http.Request) { NormalizeRequestURLPath(req) p := req.URL.Path if !strings.HasPrefix(p, stripPrefix) { orig(req) if setAuth != "" { req.Header.Set("Authorization", setAuth) } // Server-side requests carry RequestURI; client RoundTrip rejects it with URL.Host set. req.RequestURI = "" req.Header.Del("Host") req.Host = req.URL.Host return } rest := strings.TrimPrefix(p, stripPrefix) rest = strings.TrimPrefix(rest, "/") if (req.Method == http.MethodGet || req.Method == http.MethodHead) && rest == "users" { rest = "stats/users" } joined := buildUpstreamURL(target, pathPrefix, rest) req.URL.Scheme = joined.Scheme req.URL.Host = joined.Host req.URL.Path = joined.Path req.URL.RawPath = joined.RawPath req.URL.Opaque = "" // Match Host header to authority; clear stale map entry (e.g. from httptest.NewRequest). req.Header.Del("Host") req.Host = req.URL.Host if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } if setAuth != "" { req.Header.Set("Authorization", setAuth) } req.RequestURI = "" } return proxy } func buildUpstreamURL(target *url.URL, pathPrefix, rest string) *url.URL { rel := strings.Trim(pathPrefix, "/") if rest != "" { if rel != "" { rel = rel + "/" + rest } else { rel = rest } } var parts []string for _, seg := range strings.Split(rel, "/") { if seg != "" { parts = append(parts, seg) } } if len(parts) == 0 { out := *target return &out } return target.JoinPath(parts...) }