Preserve route host for managed proxy targets
- commit
- 93ddb1bfeba874a244518caf43d7937dacf01776
- author
- brice · · 2026-04-28 18:32 UTC
- parent
- bf863eb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +92 -25
M internal/adapters/in/http/proxy/forwarder.go +42 -19
| @@ -90,14 +90,19 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target | |||
| 90 | 90 | return | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | - | originalHost := target.OriginalHost | |
| 94 | 93 | releaseInFlight := h.proxySvc.TrackInFlight(target.ContainerID) | |
| 95 | 94 | defer releaseInFlight() | |
| 96 | 95 | ||
| 97 | 96 | transport := h.transportForTarget(target) | |
| 98 | 97 | ||
| 99 | - | proxy := newReverseProxy(targetURL, originalHost, transport, r, h.trustedNets, | |
| 100 | - | func(w http.ResponseWriter, _ *http.Request, proxyErr error) { | |
| 98 | + | proxy := newReverseProxy(reverseProxyOptions{ | |
| 99 | + | targetURL: targetURL, | |
| 100 | + | hostHeader: targetHostHeader(target, targetURL), | |
| 101 | + | forwardedHost: target.RouteHost, | |
| 102 | + | transport: transport, | |
| 103 | + | incomingReq: r, | |
| 104 | + | trustedNets: h.trustedNets, | |
| 105 | + | errorHandler: func(w http.ResponseWriter, _ *http.Request, proxyErr error) { | |
| 101 | 106 | var maxBytesErr *http.MaxBytesError | |
| 102 | 107 | if errors.As(proxyErr, &maxBytesErr) { | |
| 103 | 108 | log.Warn().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: request body too large") | |
| @@ -107,8 +112,8 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target | |||
| 107 | 112 | log.Error().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: connection failed") | |
| 108 | 113 | proxyError(w, "Service Unavailable", http.StatusServiceUnavailable) | |
| 109 | 114 | }, | |
| 110 | - | modifyResponse(maxResponseSize), | |
| 111 | - | ) | |
| 115 | + | modifyResponse: modifyResponse(maxResponseSize), | |
| 116 | + | }) | |
| 112 | 117 | ||
| 113 | 118 | if target.Protocol == "h2c" { | |
| 114 | 119 | log.Debug(). | |
| @@ -176,39 +181,57 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi | |||
| 176 | 181 | proxy.ServeHTTP(w, r) | |
| 177 | 182 | } | |
| 178 | 183 | ||
| 184 | + | type reverseProxyOptions struct { | |
| 185 | + | targetURL *url.URL | |
| 186 | + | hostHeader string | |
| 187 | + | forwardedHost string | |
| 188 | + | transport http.RoundTripper | |
| 189 | + | incomingReq *http.Request | |
| 190 | + | trustedNets []*net.IPNet | |
| 191 | + | errorHandler func(http.ResponseWriter, *http.Request, error) | |
| 192 | + | modifyResponse func(*http.Response) error | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | func targetHostHeader(target *domain.ProxyTarget, targetURL *url.URL) string { | |
| 196 | + | if target.RouteHost != "" { | |
| 197 | + | return target.RouteHost | |
| 198 | + | } | |
| 199 | + | if target.OriginalHost != "" { | |
| 200 | + | return net.JoinHostPort(target.OriginalHost, targetURL.Port()) | |
| 201 | + | } | |
| 202 | + | return targetURL.Host | |
| 203 | + | } | |
| 204 | + | ||
| 179 | 205 | // newReverseProxy creates a reverse proxy using Rewrite instead of Director to prevent | |
| 180 | 206 | // hop-by-hop header attacks. A malicious client could send "Connection: Authorization" | |
| 181 | 207 | // to strip the Authorization header when using the default Director. Rewrite processes | |
| 182 | 208 | // headers after hop-by-hop removal, ensuring headers like Authorization are preserved. | |
| 183 | 209 | // | |
| 184 | - | // originalHost, when non-empty, overrides the Host header sent to the backend. | |
| 185 | - | // This is used for DNS-pinned targets where the proxy dials an IP but needs to | |
| 186 | - | // send the original hostname for virtual-hosted upstreams. | |
| 187 | - | // | |
| 188 | 210 | // incomingReq and trustedNets are used to preserve X-Forwarded-Proto when the | |
| 189 | 211 | // request originates from a trusted upstream proxy. | |
| 190 | - | func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, incomingReq *http.Request, trustedNets []*net.IPNet, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy { | |
| 212 | + | func newReverseProxy(opts reverseProxyOptions) *httputil.ReverseProxy { | |
| 191 | 213 | return &httputil.ReverseProxy{ | |
| 192 | 214 | Rewrite: func(pr *httputil.ProxyRequest) { | |
| 193 | 215 | existingProto := pr.In.Header.Get("X-Forwarded-Proto") | |
| 194 | - | pr.SetURL(targetURL) | |
| 216 | + | pr.SetURL(opts.targetURL) | |
| 195 | 217 | pr.SetXForwarded() | |
| 196 | - | if originalHost != "" { | |
| 197 | - | pr.Out.Host = net.JoinHostPort(originalHost, targetURL.Port()) | |
| 198 | - | } else { | |
| 199 | - | pr.Out.Host = targetURL.Host | |
| 218 | + | if opts.hostHeader != "" { | |
| 219 | + | pr.Out.Host = opts.hostHeader | |
| 220 | + | } | |
| 221 | + | if opts.forwardedHost != "" { | |
| 222 | + | pr.Out.Header.Set("X-Forwarded-Host", opts.forwardedHost) | |
| 200 | 223 | } | |
| 201 | 224 | // Preserve X-Forwarded-Proto from trusted upstream proxies. | |
| 202 | 225 | // SetXForwarded() unconditionally sets it based on the incoming scheme | |
| 203 | 226 | // (HTTP between proxies), but when a trusted proxy already sent the | |
| 204 | 227 | // real client proto, we must honor it. | |
| 205 | - | if existingProto != "" && gordonhttp.IsTrustedSource(incomingReq, trustedNets) { | |
| 228 | + | if existingProto != "" && gordonhttp.IsTrustedSource(opts.incomingReq, opts.trustedNets) { | |
| 206 | 229 | pr.Out.Header.Set("X-Forwarded-Proto", existingProto) | |
| 207 | 230 | } | |
| 208 | 231 | }, | |
| 209 | - | Transport: transport, | |
| 210 | - | ErrorHandler: errorHandler, | |
| 211 | - | ModifyResponse: modifyResp, | |
| 232 | + | Transport: opts.transport, | |
| 233 | + | ErrorHandler: opts.errorHandler, | |
| 234 | + | ModifyResponse: opts.modifyResponse, | |
| 212 | 235 | } | |
| 213 | 236 | } | |
| 214 | 237 | ||
| @@ -90,14 +90,19 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target | |||||
| 90 | return | 90 | return | ||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | - | originalHost := target.OriginalHost | |||
| 94 | releaseInFlight := h.proxySvc.TrackInFlight(target.ContainerID) | 93 | releaseInFlight := h.proxySvc.TrackInFlight(target.ContainerID) | ||
| 95 | defer releaseInFlight() | 94 | defer releaseInFlight() | ||
| 96 | 95 | ||||
| 97 | transport := h.transportForTarget(target) | 96 | transport := h.transportForTarget(target) | ||
| 98 | 97 | ||||
| 99 | - | proxy := newReverseProxy(targetURL, originalHost, transport, r, h.trustedNets, | 98 | + | proxy := newReverseProxy(reverseProxyOptions{ |
| 100 | - | func(w http.ResponseWriter, _ *http.Request, proxyErr error) { | 99 | + | targetURL: targetURL, |
| 100 | + | hostHeader: targetHostHeader(target, targetURL), | |||
| 101 | + | forwardedHost: target.RouteHost, | |||
| 102 | + | transport: transport, | |||
| 103 | + | incomingReq: r, | |||
| 104 | + | trustedNets: h.trustedNets, | |||
| 105 | + | errorHandler: func(w http.ResponseWriter, _ *http.Request, proxyErr error) { | |||
| 101 | var maxBytesErr *http.MaxBytesError | 106 | var maxBytesErr *http.MaxBytesError | ||
| 102 | if errors.As(proxyErr, &maxBytesErr) { | 107 | if errors.As(proxyErr, &maxBytesErr) { | ||
| 103 | log.Warn().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: request body too large") | 108 | log.Warn().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: request body too large") | ||
| @@ -107,8 +112,8 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target | |||||
| 107 | log.Error().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: connection failed") | 112 | log.Error().Err(proxyErr).Str("target", targetURL.String()).Msg("proxy error: connection failed") | ||
| 108 | proxyError(w, "Service Unavailable", http.StatusServiceUnavailable) | 113 | proxyError(w, "Service Unavailable", http.StatusServiceUnavailable) | ||
| 109 | }, | 114 | }, | ||
| 110 | - | modifyResponse(maxResponseSize), | 115 | + | modifyResponse: modifyResponse(maxResponseSize), |
| 111 | - | ) | 116 | + | }) |
| 112 | 117 | ||||
| 113 | if target.Protocol == "h2c" { | 118 | if target.Protocol == "h2c" { | ||
| 114 | log.Debug(). | 119 | log.Debug(). | ||
| @@ -176,39 +181,57 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi | |||||
| 176 | proxy.ServeHTTP(w, r) | 181 | proxy.ServeHTTP(w, r) | ||
| 177 | } | 182 | } | ||
| 178 | 183 | ||||
| 184 | + | type reverseProxyOptions struct { | |||
| 185 | + | targetURL *url.URL | |||
| 186 | + | hostHeader string | |||
| 187 | + | forwardedHost string | |||
| 188 | + | transport http.RoundTripper | |||
| 189 | + | incomingReq *http.Request | |||
| 190 | + | trustedNets []*net.IPNet | |||
| 191 | + | errorHandler func(http.ResponseWriter, *http.Request, error) | |||
| 192 | + | modifyResponse func(*http.Response) error | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | func targetHostHeader(target *domain.ProxyTarget, targetURL *url.URL) string { | |||
| 196 | + | if target.RouteHost != "" { | |||
| 197 | + | return target.RouteHost | |||
| 198 | + | } | |||
| 199 | + | if target.OriginalHost != "" { | |||
| 200 | + | return net.JoinHostPort(target.OriginalHost, targetURL.Port()) | |||
| 201 | + | } | |||
| 202 | + | return targetURL.Host | |||
| 203 | + | } | |||
| 204 | + | ||||
| 179 | // newReverseProxy creates a reverse proxy using Rewrite instead of Director to prevent | 205 | // newReverseProxy creates a reverse proxy using Rewrite instead of Director to prevent | ||
| 180 | // hop-by-hop header attacks. A malicious client could send "Connection: Authorization" | 206 | // hop-by-hop header attacks. A malicious client could send "Connection: Authorization" | ||
| 181 | // to strip the Authorization header when using the default Director. Rewrite processes | 207 | // to strip the Authorization header when using the default Director. Rewrite processes | ||
| 182 | // headers after hop-by-hop removal, ensuring headers like Authorization are preserved. | 208 | // headers after hop-by-hop removal, ensuring headers like Authorization are preserved. | ||
| 183 | // | 209 | // | ||
| 184 | - | // originalHost, when non-empty, overrides the Host header sent to the backend. | |||
| 185 | - | // This is used for DNS-pinned targets where the proxy dials an IP but needs to | |||
| 186 | - | // send the original hostname for virtual-hosted upstreams. | |||
| 187 | - | // | |||
| 188 | // incomingReq and trustedNets are used to preserve X-Forwarded-Proto when the | 210 | // incomingReq and trustedNets are used to preserve X-Forwarded-Proto when the | ||
| 189 | // request originates from a trusted upstream proxy. | 211 | // request originates from a trusted upstream proxy. | ||
| 190 | - | func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, incomingReq *http.Request, trustedNets []*net.IPNet, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy { | 212 | + | func newReverseProxy(opts reverseProxyOptions) *httputil.ReverseProxy { |
| 191 | return &httputil.ReverseProxy{ | 213 | return &httputil.ReverseProxy{ | ||
| 192 | Rewrite: func(pr *httputil.ProxyRequest) { | 214 | Rewrite: func(pr *httputil.ProxyRequest) { | ||
| 193 | existingProto := pr.In.Header.Get("X-Forwarded-Proto") | 215 | existingProto := pr.In.Header.Get("X-Forwarded-Proto") | ||
| 194 | - | pr.SetURL(targetURL) | 216 | + | pr.SetURL(opts.targetURL) |
| 195 | pr.SetXForwarded() | 217 | pr.SetXForwarded() | ||
| 196 | - | if originalHost != "" { | 218 | + | if opts.hostHeader != "" { |
| 197 | - | pr.Out.Host = net.JoinHostPort(originalHost, targetURL.Port()) | 219 | + | pr.Out.Host = opts.hostHeader |
| 198 | - | } else { | 220 | + | } |
| 199 | - | pr.Out.Host = targetURL.Host | 221 | + | if opts.forwardedHost != "" { |
| 222 | + | pr.Out.Header.Set("X-Forwarded-Host", opts.forwardedHost) | |||
| 200 | } | 223 | } | ||
| 201 | // Preserve X-Forwarded-Proto from trusted upstream proxies. | 224 | // Preserve X-Forwarded-Proto from trusted upstream proxies. | ||
| 202 | // SetXForwarded() unconditionally sets it based on the incoming scheme | 225 | // SetXForwarded() unconditionally sets it based on the incoming scheme | ||
| 203 | // (HTTP between proxies), but when a trusted proxy already sent the | 226 | // (HTTP between proxies), but when a trusted proxy already sent the | ||
| 204 | // real client proto, we must honor it. | 227 | // real client proto, we must honor it. | ||
| 205 | - | if existingProto != "" && gordonhttp.IsTrustedSource(incomingReq, trustedNets) { | 228 | + | if existingProto != "" && gordonhttp.IsTrustedSource(opts.incomingReq, opts.trustedNets) { |
| 206 | pr.Out.Header.Set("X-Forwarded-Proto", existingProto) | 229 | pr.Out.Header.Set("X-Forwarded-Proto", existingProto) | ||
| 207 | } | 230 | } | ||
| 208 | }, | 231 | }, | ||
| 209 | - | Transport: transport, | 232 | + | Transport: opts.transport, |
| 210 | - | ErrorHandler: errorHandler, | 233 | + | ErrorHandler: opts.errorHandler, |
| 211 | - | ModifyResponse: modifyResp, | 234 | + | ModifyResponse: opts.modifyResponse, |
| 212 | } | 235 | } | ||
| 213 | } | 236 | } | ||
| 214 | 237 | ||||
M internal/adapters/in/http/proxy/forwarder_test.go +45 -5
| @@ -145,7 +145,44 @@ func TestForwardToTarget_OriginalHostPreserved(t *testing.T) { | |||
| 145 | 145 | handler.ServeHTTP(w, req) | |
| 146 | 146 | ||
| 147 | 147 | assert.Equal(t, http.StatusOK, w.Code) | |
| 148 | - | assert.Contains(t, receivedHost, "upstream.example.com") | |
| 148 | + | assert.Equal(t, net.JoinHostPort("upstream.example.com", strconv.Itoa(backendPort)), receivedHost) | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func TestForwardToTarget_RouteHostPreservedForManagedTarget(t *testing.T) { | |
| 152 | + | var receivedHost string | |
| 153 | + | var receivedForwardedHost string | |
| 154 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 155 | + | receivedHost = r.Host | |
| 156 | + | receivedForwardedHost = r.Header.Get("X-Forwarded-Host") | |
| 157 | + | w.WriteHeader(http.StatusOK) | |
| 158 | + | })) | |
| 159 | + | defer backend.Close() | |
| 160 | + | ||
| 161 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 162 | + | ||
| 163 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 164 | + | ||
| 165 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 166 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |
| 167 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |
| 168 | + | Host: "127.0.0.1", | |
| 169 | + | Port: backendPort, | |
| 170 | + | ContainerID: "c-1", | |
| 171 | + | Scheme: "http", | |
| 172 | + | RouteHost: "app.example.com", | |
| 173 | + | }, nil) | |
| 174 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |
| 175 | + | ||
| 176 | + | handler := NewHandler(proxySvc, nil, zerowrap.Default()) | |
| 177 | + | ||
| 178 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 179 | + | req.Host = "App.Example.com:443" | |
| 180 | + | w := httptest.NewRecorder() | |
| 181 | + | handler.ServeHTTP(w, req) | |
| 182 | + | ||
| 183 | + | assert.Equal(t, http.StatusOK, w.Code) | |
| 184 | + | assert.Equal(t, "app.example.com", receivedHost) | |
| 185 | + | assert.Equal(t, "app.example.com", receivedForwardedHost) | |
| 149 | 186 | } | |
| 150 | 187 | ||
| 151 | 188 | func TestForwardToTarget_BackendDown_Returns503(t *testing.T) { | |
| @@ -280,12 +317,15 @@ func TestNewReverseProxy_XForwardedProto(t *testing.T) { | |||
| 280 | 317 | incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto) | |
| 281 | 318 | } | |
| 282 | 319 | ||
| 283 | - | proxy := newReverseProxy(backendURL, "", http.DefaultTransport, incomingReq, tt.nets, | |
| 284 | - | func(w http.ResponseWriter, _ *http.Request, err error) { | |
| 320 | + | proxy := newReverseProxy(reverseProxyOptions{ | |
| 321 | + | targetURL: backendURL, | |
| 322 | + | transport: http.DefaultTransport, | |
| 323 | + | incomingReq: incomingReq, | |
| 324 | + | trustedNets: tt.nets, | |
| 325 | + | errorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { | |
| 285 | 326 | http.Error(w, err.Error(), http.StatusBadGateway) | |
| 286 | 327 | }, | |
| 287 | - | nil, | |
| 288 | - | ) | |
| 328 | + | }) | |
| 289 | 329 | ||
| 290 | 330 | rec := httptest.NewRecorder() | |
| 291 | 331 | proxy.ServeHTTP(rec, incomingReq) | |
| @@ -145,7 +145,44 @@ func TestForwardToTarget_OriginalHostPreserved(t *testing.T) { | |||||
| 145 | handler.ServeHTTP(w, req) | 145 | handler.ServeHTTP(w, req) | ||
| 146 | 146 | ||||
| 147 | assert.Equal(t, http.StatusOK, w.Code) | 147 | assert.Equal(t, http.StatusOK, w.Code) | ||
| 148 | - | assert.Contains(t, receivedHost, "upstream.example.com") | 148 | + | assert.Equal(t, net.JoinHostPort("upstream.example.com", strconv.Itoa(backendPort)), receivedHost) |
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | func TestForwardToTarget_RouteHostPreservedForManagedTarget(t *testing.T) { | |||
| 152 | + | var receivedHost string | |||
| 153 | + | var receivedForwardedHost string | |||
| 154 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 155 | + | receivedHost = r.Host | |||
| 156 | + | receivedForwardedHost = r.Header.Get("X-Forwarded-Host") | |||
| 157 | + | w.WriteHeader(http.StatusOK) | |||
| 158 | + | })) | |||
| 159 | + | defer backend.Close() | |||
| 160 | + | ||||
| 161 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 162 | + | ||||
| 163 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 164 | + | ||||
| 165 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 166 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |||
| 167 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |||
| 168 | + | Host: "127.0.0.1", | |||
| 169 | + | Port: backendPort, | |||
| 170 | + | ContainerID: "c-1", | |||
| 171 | + | Scheme: "http", | |||
| 172 | + | RouteHost: "app.example.com", | |||
| 173 | + | }, nil) | |||
| 174 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |||
| 175 | + | ||||
| 176 | + | handler := NewHandler(proxySvc, nil, zerowrap.Default()) | |||
| 177 | + | ||||
| 178 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 179 | + | req.Host = "App.Example.com:443" | |||
| 180 | + | w := httptest.NewRecorder() | |||
| 181 | + | handler.ServeHTTP(w, req) | |||
| 182 | + | ||||
| 183 | + | assert.Equal(t, http.StatusOK, w.Code) | |||
| 184 | + | assert.Equal(t, "app.example.com", receivedHost) | |||
| 185 | + | assert.Equal(t, "app.example.com", receivedForwardedHost) | |||
| 149 | } | 186 | } | ||
| 150 | 187 | ||||
| 151 | func TestForwardToTarget_BackendDown_Returns503(t *testing.T) { | 188 | func TestForwardToTarget_BackendDown_Returns503(t *testing.T) { | ||
| @@ -280,12 +317,15 @@ func TestNewReverseProxy_XForwardedProto(t *testing.T) { | |||||
| 280 | incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto) | 317 | incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto) | ||
| 281 | } | 318 | } | ||
| 282 | 319 | ||||
| 283 | - | proxy := newReverseProxy(backendURL, "", http.DefaultTransport, incomingReq, tt.nets, | 320 | + | proxy := newReverseProxy(reverseProxyOptions{ |
| 284 | - | func(w http.ResponseWriter, _ *http.Request, err error) { | 321 | + | targetURL: backendURL, |
| 322 | + | transport: http.DefaultTransport, | |||
| 323 | + | incomingReq: incomingReq, | |||
| 324 | + | trustedNets: tt.nets, | |||
| 325 | + | errorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { | |||
| 285 | http.Error(w, err.Error(), http.StatusBadGateway) | 326 | http.Error(w, err.Error(), http.StatusBadGateway) | ||
| 286 | }, | 327 | }, | ||
| 287 | - | nil, | 328 | + | }) |
| 288 | - | ) | |||
| 289 | 329 | ||||
| 290 | rec := httptest.NewRecorder() | 330 | rec := httptest.NewRecorder() | ||
| 291 | proxy.ServeHTTP(rec, incomingReq) | 331 | proxy.ServeHTTP(rec, incomingReq) | ||
M internal/domain/route.go +2 -1
| @@ -15,7 +15,8 @@ type ProxyTarget struct { | |||
| 15 | 15 | ContainerID string | |
| 16 | 16 | Scheme string // "http" or "https" | |
| 17 | 17 | Protocol string // "" (default HTTP/1.1) or "h2c" (cleartext HTTP/2) | |
| 18 | - | OriginalHost string // Original hostname before DNS resolution (for Host header) | |
| 18 | + | OriginalHost string // Original hostname before DNS resolution (for external-route Host header) | |
| 19 | + | RouteHost string // Canonical matched route domain for managed-route Host header | |
| 19 | 20 | } | |
| 20 | 21 | ||
| 21 | 22 | // RouteMatch represents the result of matching a request to a route. | |
| @@ -15,7 +15,8 @@ type ProxyTarget struct { | |||||
| 15 | ContainerID string | 15 | ContainerID string | ||
| 16 | Scheme string // "http" or "https" | 16 | Scheme string // "http" or "https" | ||
| 17 | Protocol string // "" (default HTTP/1.1) or "h2c" (cleartext HTTP/2) | 17 | Protocol string // "" (default HTTP/1.1) or "h2c" (cleartext HTTP/2) | ||
| 18 | - | OriginalHost string // Original hostname before DNS resolution (for Host header) | 18 | + | OriginalHost string // Original hostname before DNS resolution (for external-route Host header) |
| 19 | + | RouteHost string // Canonical matched route domain for managed-route Host header | |||
| 19 | } | 20 | } | ||
| 20 | 21 | ||||
| 21 | // RouteMatch represents the result of matching a request to a route. | 22 | // RouteMatch represents the result of matching a request to a route. | ||
M internal/usecase/proxy/service.go +2 -0
| @@ -134,6 +134,7 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||
| 134 | 134 | ContainerID: container.ID, | |
| 135 | 135 | Scheme: "http", | |
| 136 | 136 | Protocol: meta.Protocol, | |
| 137 | + | RouteHost: domainName, | |
| 137 | 138 | } | |
| 138 | 139 | } else { | |
| 139 | 140 | // Gordon is on the host - use host port mapping | |
| @@ -166,6 +167,7 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||
| 166 | 167 | ContainerID: container.ID, | |
| 167 | 168 | Scheme: "http", | |
| 168 | 169 | Protocol: meta.Protocol, | |
| 170 | + | RouteHost: domainName, | |
| 169 | 171 | } | |
| 170 | 172 | } | |
| 171 | 173 | ||
| @@ -134,6 +134,7 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||||
| 134 | ContainerID: container.ID, | 134 | ContainerID: container.ID, | ||
| 135 | Scheme: "http", | 135 | Scheme: "http", | ||
| 136 | Protocol: meta.Protocol, | 136 | Protocol: meta.Protocol, | ||
| 137 | + | RouteHost: domainName, | |||
| 137 | } | 138 | } | ||
| 138 | } else { | 139 | } else { | ||
| 139 | // Gordon is on the host - use host port mapping | 140 | // Gordon is on the host - use host port mapping | ||
| @@ -166,6 +167,7 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||||
| 166 | ContainerID: container.ID, | 167 | ContainerID: container.ID, | ||
| 167 | Scheme: "http", | 168 | Scheme: "http", | ||
| 168 | Protocol: meta.Protocol, | 169 | Protocol: meta.Protocol, | ||
| 170 | + | RouteHost: domainName, | |||
| 169 | } | 171 | } | ||
| 170 | } | 172 | } | ||
| 171 | 173 | ||||
M internal/usecase/proxy/service_test.go +1 -0
| @@ -644,6 +644,7 @@ func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | |||
| 644 | 644 | assert.Equal(t, "localhost", result.Host) | |
| 645 | 645 | assert.Equal(t, 32000, result.Port) | |
| 646 | 646 | assert.Equal(t, "c-gitea", result.ContainerID) | |
| 647 | + | assert.Equal(t, "git.example.com", result.RouteHost) | |
| 647 | 648 | } | |
| 648 | 649 | ||
| 649 | 650 | func TestService_GetTarget_HostMode_UsesDeprecatedPortLabel(t *testing.T) { | |
| @@ -644,6 +644,7 @@ func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | |||||
| 644 | assert.Equal(t, "localhost", result.Host) | 644 | assert.Equal(t, "localhost", result.Host) | ||
| 645 | assert.Equal(t, 32000, result.Port) | 645 | assert.Equal(t, 32000, result.Port) | ||
| 646 | assert.Equal(t, "c-gitea", result.ContainerID) | 646 | assert.Equal(t, "c-gitea", result.ContainerID) | ||
| 647 | + | assert.Equal(t, "git.example.com", result.RouteHost) | |||
| 647 | } | 648 | } | ||
| 648 | 649 | ||||
| 649 | func TestService_GetTarget_HostMode_UsesDeprecatedPortLabel(t *testing.T) { | 650 | func TestService_GetTarget_HostMode_UsesDeprecatedPortLabel(t *testing.T) { | ||