From 83871f27dd563cf6abec658d30a3b5dd1f904a31 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 17 Oct 2024 09:12:04 +0200 Subject: [PATCH] Add an option to preserve server path --- .../reference/dynamic-configuration/file.toml | 2 + .../reference/dynamic-configuration/file.yaml | 2 + .../reference/dynamic-configuration/kv-ref.md | 2 + docs/content/routing/services/index.md | 34 +++++- pkg/config/dynamic/http_config.go | 9 +- pkg/proxy/fast/builder.go | 4 +- pkg/proxy/fast/proxy.go | 9 +- pkg/proxy/fast/proxy_test.go | 28 ++++- pkg/proxy/fast/proxy_websocket_test.go | 6 +- pkg/proxy/httputil/builder.go | 4 +- pkg/proxy/httputil/builder_test.go | 2 +- pkg/proxy/httputil/proxy.go | 67 +++++++++--- pkg/proxy/httputil/proxy_test.go | 102 ++++++++++++++++++ pkg/proxy/httputil/proxy_websocket_test.go | 8 +- pkg/proxy/smart_builder.go | 6 +- pkg/proxy/smart_builder_test.go | 2 +- pkg/server/router/router_test.go | 2 +- pkg/server/routerfactory_test.go | 2 +- pkg/server/service/service.go | 4 +- 19 files changed, 251 insertions(+), 44 deletions(-) create mode 100644 pkg/proxy/httputil/proxy_test.go diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 18fc9bc84..065d3ca16 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -59,10 +59,12 @@ [[http.services.Service02.loadBalancer.servers]] url = "foobar" weight = 42 + preservePath = true [[http.services.Service02.loadBalancer.servers]] url = "foobar" weight = 42 + preservePath = true [http.services.Service02.loadBalancer.healthCheck] scheme = "foobar" mode = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index d0b0fc0fb..08cb9c385 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -66,8 +66,10 @@ http: servers: - url: foobar weight: 42 + preservePath: true - url: foobar weight: 42 + preservePath: true healthCheck: scheme: foobar mode: foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 01de7ee69..a2f24a805 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -256,8 +256,10 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/services/Service02/loadBalancer/healthCheck/timeout` | `42s` | | `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` | | `traefik/http/services/Service02/loadBalancer/responseForwarding/flushInterval` | `42s` | +| `traefik/http/services/Service02/loadBalancer/servers/0/preservePath` | `true` | | `traefik/http/services/Service02/loadBalancer/servers/0/url` | `foobar` | | `traefik/http/services/Service02/loadBalancer/servers/0/weight` | `42` | +| `traefik/http/services/Service02/loadBalancer/servers/1/preservePath` | `true` | | `traefik/http/services/Service02/loadBalancer/servers/1/url` | `foobar` | | `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` | | `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` | diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 8e4cc41f0..b58ac814a 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -116,12 +116,8 @@ Each service has a load-balancer, even if there is only one server to forward tr #### Servers Servers declare a single instance of your program. -The `url` option point to a specific instance. -!!! info "" - Paths in the servers' `url` have no effect. - If you want the requests to be sent to a specific path on your servers, - configure your [`routers`](../routers/index.md) to use a corresponding [middleware](../../middlewares/overview.md) (e.g. the [AddPrefix](../../middlewares/http/addprefix.md) or [ReplacePath](../../middlewares/http/replacepath.md)) middlewares. +The `url` option point to a specific instance. ??? example "A Service with One Server -- Using the [File Provider](../../providers/file.md)" @@ -173,6 +169,34 @@ The `weight` option allows for weighted load balancing on the servers. weight = 1 ``` +The `preservePath` option allows to preserve the URL path. + +!!! info "Health Check" + + When a [health check](#health-check) is configured for the server, the path is not preserved. + +??? example "A Service with One Server and PreservePath -- Using the [File Provider](../../providers/file.md)" + + ```yaml tab="YAML" + ## Dynamic configuration + http: + services: + my-service: + loadBalancer: + servers: + - url: "http://private-ip-server-1/base" + preservePath: true + ``` + + ```toml tab="TOML" + ## Dynamic configuration + [http.services] + [http.services.my-service.loadBalancer] + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/base" + preservePath = true + ``` + #### Load-balancing For now, only round robin load balancing is supported: diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index d347df81e..e29bdd30a 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -244,10 +244,11 @@ func (r *ResponseForwarding) SetDefaults() { // Server holds the server configuration. type Server struct { - URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"` - Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight"` - Scheme string `json:"-" toml:"-" yaml:"-" file:"-"` - Port string `json:"-" toml:"-" yaml:"-" file:"-"` + URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"` + Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight" export:"true"` + PreservePath bool `json:"preservePath,omitempty" toml:"preservePath,omitempty" yaml:"preservePath,omitempty" label:"-" export:"true"` + Scheme string `json:"-" toml:"-" yaml:"-" file:"-"` + Port string `json:"-" toml:"-" yaml:"-" file:"-"` } // SetDefaults Default values for a Server. diff --git a/pkg/proxy/fast/builder.go b/pkg/proxy/fast/builder.go index f330d6756..c53363dad 100644 --- a/pkg/proxy/fast/builder.go +++ b/pkg/proxy/fast/builder.go @@ -68,7 +68,7 @@ func (r *ProxyBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) { } // Build builds a new ReverseProxy with the given configuration. -func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader bool) (http.Handler, error) { +func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool) (http.Handler, error) { proxyURL, err := r.proxy(&http.Request{URL: targetURL}) if err != nil { return nil, fmt.Errorf("getting proxy: %w", err) @@ -90,7 +90,7 @@ func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader } pool := r.getPool(cfgName, cfg, tlsConfig, targetURL, proxyURL) - return NewReverseProxy(targetURL, proxyURL, r.debug, passHostHeader, responseHeaderTimeout, pool) + return NewReverseProxy(targetURL, proxyURL, r.debug, passHostHeader, preservePath, responseHeaderTimeout, pool) } func (r *ProxyBuilder) getPool(cfgName string, config *dynamic.ServersTransport, tlsConfig *tls.Config, targetURL *url.URL, proxyURL *url.URL) *connPool { diff --git a/pkg/proxy/fast/proxy.go b/pkg/proxy/fast/proxy.go index e61a32ad8..388a4d73c 100644 --- a/pkg/proxy/fast/proxy.go +++ b/pkg/proxy/fast/proxy.go @@ -121,11 +121,12 @@ type ReverseProxy struct { targetURL *url.URL passHostHeader bool + preservePath bool responseHeaderTimeout time.Duration } // NewReverseProxy creates a new ReverseProxy. -func NewReverseProxy(targetURL *url.URL, proxyURL *url.URL, debug, passHostHeader bool, responseHeaderTimeout time.Duration, connPool *connPool) (*ReverseProxy, error) { +func NewReverseProxy(targetURL, proxyURL *url.URL, debug, passHostHeader, preservePath bool, responseHeaderTimeout time.Duration, connPool *connPool) (*ReverseProxy, error) { var proxyAuth string if proxyURL != nil && proxyURL.User != nil && targetURL.Scheme == "http" { username := proxyURL.User.Username() @@ -136,6 +137,7 @@ func NewReverseProxy(targetURL *url.URL, proxyURL *url.URL, debug, passHostHeade return &ReverseProxy{ debug: debug, passHostHeader: passHostHeader, + preservePath: preservePath, targetURL: targetURL, proxyAuth: proxyAuth, connPool: connPool, @@ -207,6 +209,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { u2.Path = u.Path u2.RawPath = u.RawPath + + if p.preservePath { + u2.Path, u2.RawPath = proxyhttputil.JoinURLPath(p.targetURL, u) + } + u2.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") outReq.SetHost(u2.Host) diff --git a/pkg/proxy/fast/proxy_test.go b/pkg/proxy/fast/proxy_test.go index ee75ae0db..f4593d9ce 100644 --- a/pkg/proxy/fast/proxy_test.go +++ b/pkg/proxy/fast/proxy_test.go @@ -230,7 +230,7 @@ func TestProxyFromEnvironment(t *testing.T) { return u, nil } - reverseProxy, err := builder.Build("foo", testhelpers.MustParseURL(backendURL), false) + reverseProxy, err := builder.Build("foo", testhelpers.MustParseURL(backendURL), false, false) require.NoError(t, err) reverseProxyServer := httptest.NewServer(reverseProxy) @@ -252,6 +252,32 @@ func TestProxyFromEnvironment(t *testing.T) { } } +func TestPreservePath(t *testing.T) { + var callCount int + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + callCount++ + assert.Equal(t, "/base/foo/bar", req.URL.Path) + assert.Equal(t, "/base/foo%2Fbar", req.URL.RawPath) + })) + t.Cleanup(server.Close) + + builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{}) + + serverURL, err := url.JoinPath(server.URL, "base") + require.NoError(t, err) + + proxyHandler, err := builder.Build("", testhelpers.MustParseURL(serverURL), true, true) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/foo%2Fbar", http.NoBody) + res := httptest.NewRecorder() + + proxyHandler.ServeHTTP(res, req) + + assert.Equal(t, 1, callCount) + assert.Equal(t, http.StatusOK, res.Code) +} + func newCertificate(t *testing.T, domain string) *tls.Certificate { t.Helper() diff --git a/pkg/proxy/fast/proxy_websocket_test.go b/pkg/proxy/fast/proxy_websocket_test.go index ff2e273e9..8297ac485 100644 --- a/pkg/proxy/fast/proxy_websocket_test.go +++ b/pkg/proxy/fast/proxy_websocket_test.go @@ -362,7 +362,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) { u := parseURI(t, srv.URL) - f, err := NewReverseProxy(u, nil, true, false, 0, newConnPool(1, 0, func() (net.Conn, error) { + f, err := NewReverseProxy(u, nil, true, false, false, 0, newConnPool(1, 0, func() (net.Conn, error) { return net.Dial("tcp", u.Host) })) require.NoError(t, err) @@ -434,7 +434,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) { defer srv.Close() u := parseURI(t, srv.URL) - f, err := NewReverseProxy(u, nil, true, false, 0, newConnPool(1, 0, func() (net.Conn, error) { + f, err := NewReverseProxy(u, nil, true, false, false, 0, newConnPool(1, 0, func() (net.Conn, error) { return net.Dial("tcp", u.Host) })) require.NoError(t, err) @@ -676,7 +676,7 @@ func createProxyWithForwarder(t *testing.T, uri string, pool *connPool) *httptes t.Helper() u := parseURI(t, uri) - proxy, err := NewReverseProxy(u, nil, false, true, 0, pool) + proxy, err := NewReverseProxy(u, nil, false, true, false, 0, pool) require.NoError(t, err) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/proxy/httputil/builder.go b/pkg/proxy/httputil/builder.go index ee88bd0c3..64360517a 100644 --- a/pkg/proxy/httputil/builder.go +++ b/pkg/proxy/httputil/builder.go @@ -38,7 +38,7 @@ func NewProxyBuilder(transportManager TransportManager, semConvMetricsRegistry * func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {} // Build builds a new httputil.ReverseProxy with the given configuration. -func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) { +func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) { roundTripper, err := r.transportManager.GetRoundTripper(cfgName) if err != nil { return nil, fmt.Errorf("getting RoundTripper: %w", err) @@ -50,5 +50,5 @@ func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper) } - return buildSingleHostProxy(targetURL, passHostHeader, flushInterval, roundTripper, r.bufferPool), nil + return buildSingleHostProxy(targetURL, passHostHeader, preservePath, flushInterval, roundTripper, r.bufferPool), nil } diff --git a/pkg/proxy/httputil/builder_test.go b/pkg/proxy/httputil/builder_test.go index 8033635d2..f7ff93902 100644 --- a/pkg/proxy/httputil/builder_test.go +++ b/pkg/proxy/httputil/builder_test.go @@ -23,7 +23,7 @@ func TestEscapedPath(t *testing.T) { roundTrippers: map[string]http.RoundTripper{"default": &http.Transport{}}, } - p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), false, true, 0) + p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), false, true, false, 0) require.NoError(t, err) proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP)) diff --git a/pkg/proxy/httputil/proxy.go b/pkg/proxy/httputil/proxy.go index e0401ea1f..8ed35c23a 100644 --- a/pkg/proxy/httputil/proxy.go +++ b/pkg/proxy/httputil/proxy.go @@ -15,15 +15,17 @@ import ( "golang.org/x/net/http/httpguts" ) -// StatusClientClosedRequest non-standard HTTP status code for client disconnection. -const StatusClientClosedRequest = 499 +const ( + // StatusClientClosedRequest non-standard HTTP status code for client disconnection. + StatusClientClosedRequest = 499 -// StatusClientClosedRequestText non-standard HTTP status for client disconnection. -const StatusClientClosedRequestText = "Client Closed Request" + // StatusClientClosedRequestText non-standard HTTP status for client disconnection. + StatusClientClosedRequestText = "Client Closed Request" +) -func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler { +func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler { return &httputil.ReverseProxy{ - Director: directorBuilder(target, passHostHeader), + Director: directorBuilder(target, passHostHeader, preservePath), Transport: roundTripper, FlushInterval: flushInterval, BufferPool: bufferPool, @@ -31,7 +33,7 @@ func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval ti } } -func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Request) { +func directorBuilder(target *url.URL, passHostHeader bool, preservePath bool) func(req *http.Request) { return func(outReq *http.Request) { outReq.URL.Scheme = target.Scheme outReq.URL.Host = target.Host @@ -46,6 +48,11 @@ func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Reques outReq.URL.Path = u.Path outReq.URL.RawPath = u.RawPath + + if preservePath { + outReq.URL.Path, outReq.URL.RawPath = JoinURLPath(target, u) + } + // If a plugin/middleware adds semicolons in query params, they should be urlEncoded. outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") outReq.RequestURI = "" // Outgoing request should not have RequestURI @@ -54,7 +61,7 @@ func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Reques outReq.ProtoMajor = 1 outReq.ProtoMinor = 1 - // Do not pass client Host header unless optsetter PassHostHeader is set. + // Do not pass client Host header unless option PassHostHeader is set. if !passHostHeader { outReq.Host = outReq.URL.Host } @@ -106,6 +113,13 @@ func ErrorHandler(w http.ResponseWriter, req *http.Request, err error) { } } +func statusText(statusCode int) string { + if statusCode == StatusClientClosedRequest { + return StatusClientClosedRequestText + } + return http.StatusText(statusCode) +} + // ComputeStatusCode computes the HTTP status code according to the given error. func ComputeStatusCode(err error) int { switch { @@ -127,9 +141,38 @@ func ComputeStatusCode(err error) int { return http.StatusInternalServerError } -func statusText(statusCode int) string { - if statusCode == StatusClientClosedRequest { - return StatusClientClosedRequestText +// JoinURLPath computes the joined path and raw path of the given URLs. +// From https://github.com/golang/go/blob/b521ebb55a9b26c8824b219376c7f91f7cda6ec2/src/net/http/httputil/reverseproxy.go#L221 +func JoinURLPath(a, b *url.URL) (path, rawpath string) { + if a.RawPath == "" && b.RawPath == "" { + return singleJoiningSlash(a.Path, b.Path), "" } - return http.StatusText(statusCode) + + // Same as singleJoiningSlash, but uses EscapedPath to determine + // whether a slash should be added + apath := a.EscapedPath() + bpath := b.EscapedPath() + + aslash := strings.HasSuffix(apath, "/") + bslash := strings.HasPrefix(bpath, "/") + + switch { + case aslash && bslash: + return a.Path + b.Path[1:], apath + bpath[1:] + case !aslash && !bslash: + return a.Path + "/" + b.Path, apath + "/" + bpath + } + return a.Path + b.Path, apath + bpath +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b } diff --git a/pkg/proxy/httputil/proxy_test.go b/pkg/proxy/httputil/proxy_test.go new file mode 100644 index 000000000..3d970fc53 --- /dev/null +++ b/pkg/proxy/httputil/proxy_test.go @@ -0,0 +1,102 @@ +package httputil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traefik/traefik/v3/pkg/testhelpers" +) + +func Test_directorBuilder(t *testing.T) { + tests := []struct { + name string + target *url.URL + passHostHeader bool + preservePath bool + incomingURL string + expectedScheme string + expectedHost string + expectedPath string + expectedRawPath string + expectedQuery string + }{ + { + name: "Basic proxy", + target: testhelpers.MustParseURL("http://example.com"), + passHostHeader: false, + preservePath: false, + incomingURL: "http://localhost/test?param=value", + expectedScheme: "http", + expectedHost: "example.com", + expectedPath: "/test", + expectedQuery: "param=value", + }, + { + name: "HTTPS target", + target: testhelpers.MustParseURL("https://secure.example.com"), + passHostHeader: false, + preservePath: false, + incomingURL: "http://localhost/secure", + expectedScheme: "https", + expectedHost: "secure.example.com", + expectedPath: "/secure", + }, + { + name: "PassHostHeader", + target: testhelpers.MustParseURL("http://example.com"), + passHostHeader: true, + preservePath: false, + incomingURL: "http://original.host/test", + expectedScheme: "http", + expectedHost: "original.host", + expectedPath: "/test", + }, + { + name: "Preserve path", + target: testhelpers.MustParseURL("http://example.com/base"), + passHostHeader: false, + preservePath: true, + incomingURL: "http://localhost/foo%2Fbar", + expectedScheme: "http", + expectedHost: "example.com", + expectedPath: "/base/foo/bar", + expectedRawPath: "/base/foo%2Fbar", + }, + { + name: "Handle semicolons in query", + target: testhelpers.MustParseURL("http://example.com"), + passHostHeader: false, + preservePath: false, + incomingURL: "http://localhost/test?param1=value1;param2=value2", + expectedScheme: "http", + expectedHost: "example.com", + expectedPath: "/test", + expectedQuery: "param1=value1¶m2=value2", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + director := directorBuilder(test.target, test.passHostHeader, test.preservePath) + + req := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody) + director(req) + + assert.Equal(t, test.expectedScheme, req.URL.Scheme) + assert.Equal(t, test.expectedHost, req.Host) + assert.Equal(t, test.expectedPath, req.URL.Path) + assert.Equal(t, test.expectedRawPath, req.URL.RawPath) + assert.Equal(t, test.expectedQuery, req.URL.RawQuery) + assert.Empty(t, req.RequestURI) + assert.Equal(t, "HTTP/1.1", req.Proto) + assert.Equal(t, 1, req.ProtoMajor) + assert.Equal(t, 1, req.ProtoMinor) + assert.False(t, !test.passHostHeader && req.Host != req.URL.Host) + }) + } +} diff --git a/pkg/proxy/httputil/proxy_websocket_test.go b/pkg/proxy/httputil/proxy_websocket_test.go index dc6fa8c82..5472bcce0 100644 --- a/pkg/proxy/httputil/proxy_websocket_test.go +++ b/pkg/proxy/httputil/proxy_websocket_test.go @@ -298,9 +298,8 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) { }, } - p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, 0) + p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0) require.NoError(t, err) - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.URL = testhelpers.MustParseURL(srv.URL) w.Header().Set("HEADER-KEY", "HEADER-VALUE") @@ -355,9 +354,8 @@ func TestWebSocketUpgradeFailed(t *testing.T) { }, } - p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, 0) + p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0) require.NoError(t, err) - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { path := req.URL.Path // keep the original path @@ -588,7 +586,7 @@ func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTrip roundTrippers: map[string]http.RoundTripper{"fwd": transport}, } - p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, false, true, 0) + p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, false, true, false, 0) require.NoError(t, err) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/proxy/smart_builder.go b/pkg/proxy/smart_builder.go index 1abadcca7..08b247c53 100644 --- a/pkg/proxy/smart_builder.go +++ b/pkg/proxy/smart_builder.go @@ -45,7 +45,7 @@ func (b *SmartBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) { } // Build builds an HTTP proxy for the given URL using the ServersTransport with the given name. -func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) { +func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) { serversTransport, err := b.transportManager.Get(configName) if err != nil { return nil, fmt.Errorf("getting ServersTransport: %w", err) @@ -55,7 +55,7 @@ func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserv // For the https scheme we cannot guess if the backend communication will use HTTP2, // thus we check if HTTP/2 is disabled to use the fast proxy implementation when this is possible. if targetURL.Scheme == "h2c" || (targetURL.Scheme == "https" && !serversTransport.DisableHTTP2) { - return b.proxyBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, flushInterval) + return b.proxyBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, preservePath, flushInterval) } - return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader) + return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader, preservePath) } diff --git a/pkg/proxy/smart_builder_test.go b/pkg/proxy/smart_builder_test.go index d1c29ddd8..c03bd19f3 100644 --- a/pkg/proxy/smart_builder_test.go +++ b/pkg/proxy/smart_builder_test.go @@ -101,7 +101,7 @@ func TestSmartBuilder_Build(t *testing.T) { httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil) proxyBuilder := NewSmartBuilder(transportManager, httpProxyBuilder, test.fastProxyConfig) - proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, time.Second) + proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, false, time.Second) require.NoError(t, err) rw := httptest.NewRecorder() diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index b545bd2da..70808e9c6 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -897,7 +897,7 @@ func BenchmarkService(b *testing.B) { type proxyBuilderMock struct{} -func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) { +func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) { return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil } diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index 6c0861d38..2b3d6fe1f 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -254,7 +254,7 @@ func TestInternalServices(t *testing.T) { type proxyBuilderMock struct{} -func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) { +func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) { return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil } diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index e37ecdf09..349c03179 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -42,7 +42,7 @@ const ( // ProxyBuilder builds reverse proxy handlers. type ProxyBuilder interface { - Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) + Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) Update(configs map[string]*dynamic.ServersTransport) } @@ -338,7 +338,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName) shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName) - proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, flushInterval) + proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, server.PreservePath, flushInterval) if err != nil { return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err) }