From e1293fb0ad791e7d4109c8e3b45cbac4d3a4f9c1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:22:51 +0200 Subject: [PATCH] The route proxy tells a backend the request was HTTPS, and for which name NewSingleHostReverseProxy sets only X-Forwarded-For, so a backend that writes its own addresses saw the plain hop from the proxy: Gitea's Go import tag named an http clone URL and go get refused the SDK's module path. The proxy now sets X-Forwarded-Proto, -Host and -For from the request it received, and keeps the Host header as it was. --- examples/route-proxy/forwarded_test.go | 29 ++++++++++++++++++++++++++ examples/route-proxy/main.go | 16 +++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 examples/route-proxy/forwarded_test.go diff --git a/examples/route-proxy/forwarded_test.go b/examples/route-proxy/forwarded_test.go new file mode 100644 index 0000000..0c31068 --- /dev/null +++ b/examples/route-proxy/forwarded_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "crypto/tls" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +// A backend behind the proxy learns the client used TLS and which name it asked for, so the addresses +// it writes into its own pages are the ones a client can use (2026-10-03: a forge's Go import tag +// named an http clone URL, and Go refused the module path). +func TestABackendIsToldTheRequestWasHTTPSAndForWhichName(t *testing.T) { + var proto, host, fwdHost, fwdFor string + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proto, host, fwdHost, fwdFor = r.Header.Get("X-Forwarded-Proto"), r.Host, r.Header.Get("X-Forwarded-Host"), r.Header.Get("X-Forwarded-For") + })) + defer backend.Close() + where, _ := url.Parse(backend.URL) + req := httptest.NewRequest(http.MethodGet, "https://git.example.org/novox/mesh-sdk/go?go-get=1", nil) + req.TLS = &tls.ConnectionState{} + req.Host = "git.example.org" + req.RemoteAddr = "192.0.2.7:51000" + towards(where).ServeHTTP(httptest.NewRecorder(), req) + if proto != "https" || fwdHost != "git.example.org" || host != "git.example.org" || fwdFor != "192.0.2.7" { + t.Errorf("the backend was told proto=%q host=%q forwarded-host=%q for=%q", proto, host, fwdHost, fwdFor) + } +} diff --git a/examples/route-proxy/main.go b/examples/route-proxy/main.go index 4a12f3a..ebadf63 100644 --- a/examples/route-proxy/main.go +++ b/examples/route-proxy/main.go @@ -273,7 +273,7 @@ func (t *table) set(routes map[string][]rule, public map[string]bool) { log.Printf("route %s points at %q, which is not a URL: %v", host, r.target, err) continue } - r.to = httputil.NewSingleHostReverseProxy(where) + r.to = towards(where) if r.insecure { r.to.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} } @@ -1060,3 +1060,17 @@ func asPort(v any) (int, bool) { } return 0, false } + +// towards proxies to one backend and tells it what the client asked: **X-Forwarded-Proto, -Host and +// -For**, set from the request this proxy received. A backend that builds its own addresses — a forge +// writing its clone URL into a page, a login redirect — otherwise sees the plain HTTP hop from this +// proxy and writes `http://`, though every client reached it over TLS: Go refused the forge's module +// path for exactly that on 2026-10-03, its import tag naming an http clone URL. +// The standard library's NewSingleHostReverseProxy sets only X-Forwarded-For. +func towards(where *url.URL) *httputil.ReverseProxy { + return &httputil.ReverseProxy{Rewrite: func(pr *httputil.ProxyRequest) { + pr.SetURL(where) + pr.Out.Host = pr.In.Host + pr.SetXForwarded() + }} +}