mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
fix(handler/http): resolve origin-form requests via Host header (#679)
Origin-form requests ("GET / HTTP/1.1" with only a Host header, as
sent by nginx proxy_pass to an upstream proxy) failed RoundTrip with
"http: no Host in request URL": normalizeRequest infers URL.Scheme
but leaves URL.Host empty, and http.Transport requires a non-empty
URL.Host. Also affected every keep-alive request after the first,
which bypass normalizeRequest entirely.
Fill scheme+Host from req.Host in the shared proxyRoundTrip path,
covering both initial and subsequent requests.
e2e-verified: raw origin-form request proxied by Host header
(gost/tests/e2e TestHTTPProxyOriginForm).
This commit is contained in:
@@ -98,6 +98,18 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriteCloser,
|
||||
}
|
||||
req.Header.Del("Gost-Record")
|
||||
|
||||
// Origin-form requests ("GET / HTTP/1.1" + Host header, e.g. sent by
|
||||
// nginx proxy_pass) carry their target only in req.Host, which
|
||||
// normalizeRequest doesn't mirror into the URL (it fills Scheme only,
|
||||
// leaving URL.Host empty). Subsequent keep-alive requests parsed in
|
||||
// handleProxy never pass through normalizeRequest at all. The
|
||||
// http.Transport requires a non-empty URL.Host, otherwise it fails
|
||||
// with "http: no Host in request URL" (#679).
|
||||
if req.URL.Host == "" {
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = req.Host
|
||||
}
|
||||
|
||||
host := normalizeHostPort(req.Host, "80")
|
||||
ro.Host = host
|
||||
ro.Time = time.Now()
|
||||
|
||||
@@ -245,6 +245,48 @@ func TestProxyRoundTrip_HeaderCleanup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyRoundTrip_OriginFormHost(t *testing.T) {
|
||||
// Origin-form request (GET / + Host header), as sent by nginx
|
||||
// proxy_pass. Mirrors go-gost/gost#679: the transport must not fail
|
||||
// with "http: no Host in request URL".
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host != "myoniondomain.onion" {
|
||||
t.Errorf("unexpected Host header: %q", r.Host)
|
||||
}
|
||||
w.Header().Set("Connection", "close")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Transport must dial the test server regardless of the target host,
|
||||
// like the handler's real transport does via the proxy chain.
|
||||
h := &httpHandler{
|
||||
options: handler.Options{
|
||||
Logger: &testLogger{},
|
||||
},
|
||||
transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, ts.Listener.Addr().String())
|
||||
},
|
||||
},
|
||||
}
|
||||
h.md.readTimeout = 15
|
||||
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Host = "myoniondomain.onion"
|
||||
ro := &xrecorder.HandlerRecorderObject{
|
||||
RemoteAddr: "127.0.0.1:12345",
|
||||
}
|
||||
pStats := xstats.Stats{}
|
||||
|
||||
rw := &testReadWriteCloser{buf: new(strings.Builder)}
|
||||
|
||||
_, err := h.proxyRoundTrip(context.Background(), rw, req, ro, &pStats, &testLogger{})
|
||||
if err != nil {
|
||||
t.Fatalf("proxyRoundTrip error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// testBypass implements bypass.Bypass for testing.
|
||||
type testBypass struct {
|
||||
contains bool
|
||||
|
||||
Reference in New Issue
Block a user