From 16b1d8148ddb14cef11759da032d0b815eadb2cb Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 30 Mar 2026 00:04:45 +0700 Subject: [PATCH] Refactor reverse proxy to support API base path and update example configuration. Changed base URLs in config.example.yaml to use HTTPS and the new /api/ prefix. Enhanced reverse proxy logic in reverse.go to build upstream paths correctly and added a new test for path rewriting with the API base path in reverse_test.go. --- config.example.yaml | 8 ++--- internal/proxy/reverse.go | 55 +++++++++++++++++++++++++++------- internal/proxy/reverse_test.go | 21 +++++++++++++ 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index dea03d8..9b79c68 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -29,15 +29,15 @@ servers: path_prefix: /v1 # authorization_env: TELEMT_API_AUTH - # ivx: порт 9091 как у типичного Telemt API; при необходимости — https и другой порт. + # ivx: API за префиксом /api/ на HTTPS. - alias: gt1 - base_url: http://gt1.ivx.su:9091 + base_url: https://gt1.ivx.su/api/ path_prefix: /v1 - alias: gt2 - base_url: http://gt2.ivx.su:9091 + base_url: https://gt2.ivx.su/api/ path_prefix: /v1 - alias: gt3 - base_url: http://gt3.ivx.su:9091 + base_url: https://gt3.ivx.su/api/ path_prefix: /v1 diff --git a/internal/proxy/reverse.go b/internal/proxy/reverse.go index ed2d261..f3fb4b3 100644 --- a/internal/proxy/reverse.go +++ b/internal/proxy/reverse.go @@ -4,26 +4,37 @@ import ( "net/http" "net/http/httputil" "net/url" + "path" "strings" ) // NewReverseProxy builds a reverse proxy to target base URL with path rewriting: -// stripPrefix (/api/{alias}) + pathPrefix (/v1) + remainder. +// stripPrefix (/api/{alias}) + pathPrefix (/v1) + remainder, prepended to target.Path +// (so base_url https://host/api/ yields upstream /api/v1/...). func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth string) *httputil.ReverseProxy { proxy := httputil.NewSingleHostReverseProxy(target) orig := proxy.Director + targetQuery := target.RawQuery proxy.Director = func(req *http.Request) { - orig(req) p := req.URL.Path - if strings.HasPrefix(p, stripPrefix) { - rest := strings.TrimPrefix(p, stripPrefix) - rest = strings.TrimPrefix(rest, "/") - if rest == "" { - req.URL.Path = pathPrefix - } else { - req.URL.Path = pathPrefix + "/" + rest + if !strings.HasPrefix(p, stripPrefix) { + orig(req) + if setAuth != "" { + req.Header.Set("Authorization", setAuth) } - req.URL.RawPath = "" + return + } + rest := strings.TrimPrefix(p, stripPrefix) + rest = strings.TrimPrefix(rest, "/") + upPath := buildUpstreamPath(target.Path, pathPrefix, rest) + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path = upPath + req.URL.RawPath = "" + 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) @@ -31,3 +42,27 @@ func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth st } return proxy } + +func buildUpstreamPath(targetPath, pathPrefix, rest string) string { + base := strings.TrimSuffix(targetPath, "/") + p := strings.Trim(pathPrefix, "/") + r := strings.Trim(rest, "/") + var segs []string + if base != "" { + segs = append(segs, base) + } + if p != "" { + segs = append(segs, p) + } + if r != "" { + segs = append(segs, r) + } + if len(segs) == 0 { + return "/" + } + out := path.Join(segs...) + if !strings.HasPrefix(out, "/") { + out = "/" + out + } + return out +} diff --git a/internal/proxy/reverse_test.go b/internal/proxy/reverse_test.go index e46caae..ccf0afc 100644 --- a/internal/proxy/reverse_test.go +++ b/internal/proxy/reverse_test.go @@ -24,3 +24,24 @@ func TestReverseProxyPathRewrite(t *testing.T) { t.Fatalf("status %d", rec.Code) } } + +func TestReverseProxyPathRewriteWithAPIBasePath(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/health" { + t.Fatalf("path %q", r.URL.Path) + } + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + up, err := url.Parse(srv.URL + "/api/") + if err != nil { + t.Fatal(err) + } + rp := NewReverseProxy(up, "/api/main_srv", "/v1", "") + req := httptest.NewRequest(http.MethodGet, "/api/main_srv/health", nil) + rec := httptest.NewRecorder() + rp.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } +}