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
|
@ -59,10 +59,12 @@
|
||||||
[[http.services.Service02.loadBalancer.servers]]
|
[[http.services.Service02.loadBalancer.servers]]
|
||||||
url = "foobar"
|
url = "foobar"
|
||||||
weight = 42
|
weight = 42
|
||||||
|
preservePath = true
|
||||||
|
|
||||||
[[http.services.Service02.loadBalancer.servers]]
|
[[http.services.Service02.loadBalancer.servers]]
|
||||||
url = "foobar"
|
url = "foobar"
|
||||||
weight = 42
|
weight = 42
|
||||||
|
preservePath = true
|
||||||
[http.services.Service02.loadBalancer.healthCheck]
|
[http.services.Service02.loadBalancer.healthCheck]
|
||||||
scheme = "foobar"
|
scheme = "foobar"
|
||||||
mode = "foobar"
|
mode = "foobar"
|
||||||
|
|
|
@ -66,8 +66,10 @@ http:
|
||||||
servers:
|
servers:
|
||||||
- url: foobar
|
- url: foobar
|
||||||
weight: 42
|
weight: 42
|
||||||
|
preservePath: true
|
||||||
- url: foobar
|
- url: foobar
|
||||||
weight: 42
|
weight: 42
|
||||||
|
preservePath: true
|
||||||
healthCheck:
|
healthCheck:
|
||||||
scheme: foobar
|
scheme: foobar
|
||||||
mode: foobar
|
mode: foobar
|
||||||
|
|
|
@ -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/healthCheck/timeout` | `42s` |
|
||||||
| `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` |
|
| `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` |
|
||||||
| `traefik/http/services/Service02/loadBalancer/responseForwarding/flushInterval` | `42s` |
|
| `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/url` | `foobar` |
|
||||||
| `traefik/http/services/Service02/loadBalancer/servers/0/weight` | `42` |
|
| `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/url` | `foobar` |
|
||||||
| `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` |
|
| `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` |
|
||||||
| `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` |
|
| `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` |
|
||||||
|
|
|
@ -116,12 +116,8 @@ Each service has a load-balancer, even if there is only one server to forward tr
|
||||||
#### Servers
|
#### Servers
|
||||||
|
|
||||||
Servers declare a single instance of your program.
|
Servers declare a single instance of your program.
|
||||||
The `url` option point to a specific instance.
|
|
||||||
|
|
||||||
!!! info ""
|
The `url` option point to a specific instance.
|
||||||
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.
|
|
||||||
|
|
||||||
??? example "A Service with One Server -- Using the [File Provider](../../providers/file.md)"
|
??? 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
|
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
|
#### Load-balancing
|
||||||
|
|
||||||
For now, only round robin load balancing is supported:
|
For now, only round robin load balancing is supported:
|
||||||
|
|
|
@ -244,10 +244,11 @@ func (r *ResponseForwarding) SetDefaults() {
|
||||||
|
|
||||||
// Server holds the server configuration.
|
// Server holds the server configuration.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"`
|
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"`
|
Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight" export:"true"`
|
||||||
Scheme string `json:"-" toml:"-" yaml:"-" file:"-"`
|
PreservePath bool `json:"preservePath,omitempty" toml:"preservePath,omitempty" yaml:"preservePath,omitempty" label:"-" export:"true"`
|
||||||
Port string `json:"-" toml:"-" yaml:"-" file:"-"`
|
Scheme string `json:"-" toml:"-" yaml:"-" file:"-"`
|
||||||
|
Port string `json:"-" toml:"-" yaml:"-" file:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults Default values for a Server.
|
// SetDefaults Default values for a Server.
|
||||||
|
|
|
@ -68,7 +68,7 @@ func (r *ProxyBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a new ReverseProxy with the given configuration.
|
// 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})
|
proxyURL, err := r.proxy(&http.Request{URL: targetURL})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting proxy: %w", err)
|
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)
|
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 {
|
func (r *ProxyBuilder) getPool(cfgName string, config *dynamic.ServersTransport, tlsConfig *tls.Config, targetURL *url.URL, proxyURL *url.URL) *connPool {
|
||||||
|
|
|
@ -121,11 +121,12 @@ type ReverseProxy struct {
|
||||||
|
|
||||||
targetURL *url.URL
|
targetURL *url.URL
|
||||||
passHostHeader bool
|
passHostHeader bool
|
||||||
|
preservePath bool
|
||||||
responseHeaderTimeout time.Duration
|
responseHeaderTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReverseProxy creates a new ReverseProxy.
|
// 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
|
var proxyAuth string
|
||||||
if proxyURL != nil && proxyURL.User != nil && targetURL.Scheme == "http" {
|
if proxyURL != nil && proxyURL.User != nil && targetURL.Scheme == "http" {
|
||||||
username := proxyURL.User.Username()
|
username := proxyURL.User.Username()
|
||||||
|
@ -136,6 +137,7 @@ func NewReverseProxy(targetURL *url.URL, proxyURL *url.URL, debug, passHostHeade
|
||||||
return &ReverseProxy{
|
return &ReverseProxy{
|
||||||
debug: debug,
|
debug: debug,
|
||||||
passHostHeader: passHostHeader,
|
passHostHeader: passHostHeader,
|
||||||
|
preservePath: preservePath,
|
||||||
targetURL: targetURL,
|
targetURL: targetURL,
|
||||||
proxyAuth: proxyAuth,
|
proxyAuth: proxyAuth,
|
||||||
connPool: connPool,
|
connPool: connPool,
|
||||||
|
@ -207,6 +209,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
u2.Path = u.Path
|
u2.Path = u.Path
|
||||||
u2.RawPath = u.RawPath
|
u2.RawPath = u.RawPath
|
||||||
|
|
||||||
|
if p.preservePath {
|
||||||
|
u2.Path, u2.RawPath = proxyhttputil.JoinURLPath(p.targetURL, u)
|
||||||
|
}
|
||||||
|
|
||||||
u2.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
u2.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
||||||
|
|
||||||
outReq.SetHost(u2.Host)
|
outReq.SetHost(u2.Host)
|
||||||
|
|
|
@ -230,7 +230,7 @@ func TestProxyFromEnvironment(t *testing.T) {
|
||||||
return u, nil
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
reverseProxyServer := httptest.NewServer(reverseProxy)
|
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 {
|
func newCertificate(t *testing.T, domain string) *tls.Certificate {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
|
||||||
|
|
||||||
u := parseURI(t, srv.URL)
|
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)
|
return net.Dial("tcp", u.Host)
|
||||||
}))
|
}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -434,7 +434,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
u := parseURI(t, srv.URL)
|
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)
|
return net.Dial("tcp", u.Host)
|
||||||
}))
|
}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -676,7 +676,7 @@ func createProxyWithForwarder(t *testing.T, uri string, pool *connPool) *httptes
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
u := parseURI(t, uri)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ func NewProxyBuilder(transportManager TransportManager, semConvMetricsRegistry *
|
||||||
func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
|
func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
|
||||||
|
|
||||||
// Build builds a new httputil.ReverseProxy with the given configuration.
|
// 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)
|
roundTripper, err := r.transportManager.GetRoundTripper(cfgName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting RoundTripper: %w", err)
|
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)
|
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{}},
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP))
|
proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP))
|
||||||
|
|
|
@ -15,15 +15,17 @@ import (
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
const (
|
||||||
const StatusClientClosedRequest = 499
|
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
||||||
|
StatusClientClosedRequest = 499
|
||||||
|
|
||||||
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
||||||
const StatusClientClosedRequestText = "Client Closed Request"
|
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{
|
return &httputil.ReverseProxy{
|
||||||
Director: directorBuilder(target, passHostHeader),
|
Director: directorBuilder(target, passHostHeader, preservePath),
|
||||||
Transport: roundTripper,
|
Transport: roundTripper,
|
||||||
FlushInterval: flushInterval,
|
FlushInterval: flushInterval,
|
||||||
BufferPool: bufferPool,
|
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) {
|
return func(outReq *http.Request) {
|
||||||
outReq.URL.Scheme = target.Scheme
|
outReq.URL.Scheme = target.Scheme
|
||||||
outReq.URL.Host = target.Host
|
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.Path = u.Path
|
||||||
outReq.URL.RawPath = u.RawPath
|
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.
|
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
||||||
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
||||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
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.ProtoMajor = 1
|
||||||
outReq.ProtoMinor = 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 {
|
if !passHostHeader {
|
||||||
outReq.Host = outReq.URL.Host
|
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.
|
// ComputeStatusCode computes the HTTP status code according to the given error.
|
||||||
func ComputeStatusCode(err error) int {
|
func ComputeStatusCode(err error) int {
|
||||||
switch {
|
switch {
|
||||||
|
@ -127,9 +141,38 @@ func ComputeStatusCode(err error) int {
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusText(statusCode int) string {
|
// JoinURLPath computes the joined path and raw path of the given URLs.
|
||||||
if statusCode == StatusClientClosedRequest {
|
// From https://github.com/golang/go/blob/b521ebb55a9b26c8824b219376c7f91f7cda6ec2/src/net/http/httputil/reverseproxy.go#L221
|
||||||
return StatusClientClosedRequestText
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
req.URL = testhelpers.MustParseURL(srv.URL)
|
req.URL = testhelpers.MustParseURL(srv.URL)
|
||||||
w.Header().Set("HEADER-KEY", "HEADER-VALUE")
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
path := req.URL.Path // keep the original path
|
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},
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
|
@ -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.
|
// 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)
|
serversTransport, err := b.transportManager.Get(configName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting ServersTransport: %w", err)
|
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,
|
// 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.
|
// 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) {
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ func TestSmartBuilder_Build(t *testing.T) {
|
||||||
httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil)
|
httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil)
|
||||||
proxyBuilder := NewSmartBuilder(transportManager, httpProxyBuilder, test.fastProxyConfig)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
|
|
@ -897,7 +897,7 @@ func BenchmarkService(b *testing.B) {
|
||||||
|
|
||||||
type proxyBuilderMock struct{}
|
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
|
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,7 +254,7 @@ func TestInternalServices(t *testing.T) {
|
||||||
|
|
||||||
type proxyBuilderMock struct{}
|
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
|
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ const (
|
||||||
|
|
||||||
// ProxyBuilder builds reverse proxy handlers.
|
// ProxyBuilder builds reverse proxy handlers.
|
||||||
type ProxyBuilder interface {
|
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)
|
Update(configs map[string]*dynamic.ServersTransport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +338,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||||
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
|
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
|
||||||
|
|
||||||
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
|
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue