Add an option to preserve server path
This commit is contained in:
parent
6e1f5dc071
commit
83871f27dd
19 changed files with 251 additions and 44 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
102
pkg/proxy/httputil/proxy_test.go
Normal file
102
pkg/proxy/httputil/proxy_test.go
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue