diff --git a/config.example.yaml b/config.example.yaml index 9b79c68..c021f29 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -29,7 +29,7 @@ servers: path_prefix: /v1 # authorization_env: TELEMT_API_AUTH - # ivx: API за префиксом /api/ на HTTPS. + # ivx: HTTPS + nginx location /api/ → Telemt; base_url должен заканчиваться на /api/ - alias: gt1 base_url: https://gt1.ivx.su/api/ path_prefix: /v1 diff --git a/docs/GATEWAY_RUN.md b/docs/GATEWAY_RUN.md index 60de1e0..64a2b31 100644 --- a/docs/GATEWAY_RUN.md +++ b/docs/GATEWAY_RUN.md @@ -178,6 +178,9 @@ docker compose down ## Устранение неполадок +- **Nginx с `location /api/` и `proxy_pass http://…:9091/;` (со слэшем в конце)** на бэкенд уходит путь **без** префикса `/api/` (например запрос к nginx `GET /api/v1/users` превращается в `GET /v1/users` на Telemt). Шлюз при `base_url: https://gt2.example/api/` должен запрашивать именно **`/api/v1/…`** на стороне nginx. Если в `base_url` нет пути `/api/` (только `https://gt2.example`), шлюз обратится к `https://gt2.example/v1/…` — часто это **не** попадает в `location /api/`, и nginx отдаёт **чужой vhost / заглушку**. Задавайте `base_url` с завершающим слэшем: `https://gt2.example/api/`. +- **Заголовок `Host`**: шлюз выставляет `Host` равным хосту из `base_url` (как у обычного клиента к этому имени). Если после обновления образа проблема остаётся, с хоста шлюза проверьте: `curl -sv -o /dev/null https://gt2…/api/v1/health` и сравните с запросом через шлюз. +- **`/v1/users` и `/v1/stats/users`** — разные маршруты в Telemt Control API. То, что открывается как `…/api/v1/stats/users` в браузере, через шлюз соответствует **`/api/{alias}/stats/users`**, а не `/api/{alias}/users`. См. матрицу путей в [API.md](API.md). - **`docker pull`: `unauthorized` / `denied`**: выполните `docker login git.shts.su` с учётной записью Gitea и PAT с **`read:package`**. - **`403 forbidden` с хоста при `allow_all: false`**: добавьте CIDR клиента в `whitelist_cidrs`. Запросы из контейнера к самому себе идут с `127.0.0.1` — при необходимости добавьте `127.0.0.1/32`. - **За reverse proxy**: укажите CIDR прокси в `trusted_proxies`, иначе whitelist видит IP прокси, а не клиента. diff --git a/internal/proxy/reverse.go b/internal/proxy/reverse.go index f3fb4b3..c210c1b 100644 --- a/internal/proxy/reverse.go +++ b/internal/proxy/reverse.go @@ -4,13 +4,12 @@ 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, prepended to target.Path -// (so base_url https://host/api/ yields upstream /api/v1/...). +// stripPrefix (/api/{alias}) + pathPrefix (/v1) + remainder, joined onto target via url.JoinPath +// (e.g. https://host/api/ + v1 + users → https://host/api/v1/users). func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth string) *httputil.ReverseProxy { proxy := httputil.NewSingleHostReverseProxy(target) orig := proxy.Director @@ -26,11 +25,14 @@ func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth st } 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 = "" + 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 = "" + // Client must send the same Host as the TLS SNI / nginx server_name. + req.Host = joined.Host if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { @@ -43,26 +45,24 @@ 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) +func buildUpstreamURL(target *url.URL, pathPrefix, rest string) *url.URL { + rel := strings.Trim(pathPrefix, "/") + if rest != "" { + if rel != "" { + rel = rel + "/" + rest + } else { + rel = rest + } } - if p != "" { - segs = append(segs, p) + var parts []string + for _, seg := range strings.Split(rel, "/") { + if seg != "" { + parts = append(parts, seg) + } } - if r != "" { - segs = append(segs, r) + if len(parts) == 0 { + out := *target + return &out } - if len(segs) == 0 { - return "/" - } - out := path.Join(segs...) - if !strings.HasPrefix(out, "/") { - out = "/" + out - } - return out + return target.JoinPath(parts...) } diff --git a/internal/proxy/reverse_test.go b/internal/proxy/reverse_test.go index ccf0afc..a27dbea 100644 --- a/internal/proxy/reverse_test.go +++ b/internal/proxy/reverse_test.go @@ -45,3 +45,24 @@ func TestReverseProxyPathRewriteWithAPIBasePath(t *testing.T) { t.Fatalf("status %d", rec.Code) } } + +func TestReverseProxyPathNestedStatsUsers(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/stats/users" { + 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/gt2", "/v1", "") + req := httptest.NewRequest(http.MethodGet, "/api/gt2/stats/users", nil) + rec := httptest.NewRecorder() + rp.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } +}