mirror of
https://github.com/traefik/traefik.git
synced 2026-04-15 10:41:11 +02:00
Nginx x-forwarded-prefix annotation
This commit is contained in:
parent
efcc60fbdb
commit
ee07a31ae3
@ -339,6 +339,7 @@ The following annotations are organized by category for easier navigation.
|
||||
| <a id="opt-nginx-ingress-kubernetes-iocanary-by-cookie" href="#opt-nginx-ingress-kubernetes-iocanary-by-cookie" title="#opt-nginx-ingress-kubernetes-iocanary-by-cookie">`nginx.ingress.kubernetes.io/canary-by-cookie`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight" href="#opt-nginx-ingress-kubernetes-iocanary-weight" title="#opt-nginx-ingress-kubernetes-iocanary-weight">`nginx.ingress.kubernetes.io/canary-weight`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight-total" href="#opt-nginx-ingress-kubernetes-iocanary-weight-total" title="#opt-nginx-ingress-kubernetes-iocanary-weight-total">`nginx.ingress.kubernetes.io/canary-weight-total`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
|
||||
|
||||
### CORS
|
||||
|
||||
@ -488,7 +489,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
|
||||
| <a id="opt-nginx-ingress-kubernetes-iomirror-request-body" href="#opt-nginx-ingress-kubernetes-iomirror-request-body" title="#opt-nginx-ingress-kubernetes-iomirror-request-body">`nginx.ingress.kubernetes.io/mirror-request-body`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iomirror-target" href="#opt-nginx-ingress-kubernetes-iomirror-target" title="#opt-nginx-ingress-kubernetes-iomirror-target">`nginx.ingress.kubernetes.io/mirror-target`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iomirror-host" href="#opt-nginx-ingress-kubernetes-iomirror-host" title="#opt-nginx-ingress-kubernetes-iomirror-host">`nginx.ingress.kubernetes.io/mirror-host`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iodenylist-source-range" href="#opt-nginx-ingress-kubernetes-iodenylist-source-range" title="#opt-nginx-ingress-kubernetes-iodenylist-source-range">`nginx.ingress.kubernetes.io/denylist-source-range`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iostream-snippet" href="#opt-nginx-ingress-kubernetes-iostream-snippet" title="#opt-nginx-ingress-kubernetes-iostream-snippet">`nginx.ingress.kubernetes.io/stream-snippet`</a> | |
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ type Middleware struct {
|
||||
// ingress-nginx middlewares.
|
||||
AuthTLSPassCertificateToUpstream *AuthTLSPassCertificateToUpstream `json:"authTLSPassCertificateToUpstream,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
Snippet *Snippet `json:"snippet,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
RewriteTarget *RewriteTarget `json:"rewriteTarget,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
@ -904,3 +905,16 @@ type Snippet struct {
|
||||
ServerSnippet string `json:"serverSnippet,omitempty"`
|
||||
ConfigurationSnippet string `json:"configurationSnippet,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// RewriteTarget holds the rewrite target middleware configuration used by ingress-nginx provider.
|
||||
// This middleware replaces the path of a URL.
|
||||
type RewriteTarget struct {
|
||||
// Regex defines the regular expression used to match and capture the path from the request URL.
|
||||
Regex string `json:"regex,omitempty"`
|
||||
// Replacement defines the replacement path format, which can include captured variables.
|
||||
Replacement string `json:"replacement,omitempty"`
|
||||
// XForwardedPrefix defines the value of the X-Forwarded-Prefix header.
|
||||
XForwardedPrefix string `json:"xForwardedPrefix,omitempty"`
|
||||
}
|
||||
|
||||
@ -1122,6 +1122,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
|
||||
*out = new(Snippet)
|
||||
**out = **in
|
||||
}
|
||||
if in.RewriteTarget != nil {
|
||||
in, out := &in.RewriteTarget, &out.RewriteTarget
|
||||
*out = new(RewriteTarget)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -1491,6 +1496,22 @@ func (in *Retry) DeepCopy() *Retry {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RewriteTarget) DeepCopyInto(out *RewriteTarget) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteTarget.
|
||||
func (in *RewriteTarget) DeepCopy() *RewriteTarget {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RewriteTarget)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Router) DeepCopyInto(out *Router) {
|
||||
*out = *in
|
||||
|
||||
100
pkg/middlewares/ingressnginx/rewritetarget/rewrite_target.go
Normal file
100
pkg/middlewares/ingressnginx/rewritetarget/rewrite_target.go
Normal file
@ -0,0 +1,100 @@
|
||||
package rewritetarget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
|
||||
)
|
||||
|
||||
const (
|
||||
typeName = "RewriteTarget"
|
||||
xForwardedPrefixHeader = "X-Forwarded-Prefix"
|
||||
)
|
||||
|
||||
// RewriteTarget is a middleware used to replace the path of a URL request.
|
||||
type rewriteTarget struct {
|
||||
next http.Handler
|
||||
regexp *regexp.Regexp
|
||||
replacement string
|
||||
xForwardedPrefix string
|
||||
name string
|
||||
}
|
||||
|
||||
// New creates a new rewrite target middleware.
|
||||
func New(ctx context.Context, next http.Handler, config dynamic.RewriteTarget, name string) (http.Handler, error) {
|
||||
middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware")
|
||||
|
||||
if config.Replacement == "" {
|
||||
return nil, errors.New("replacement cannot be empty")
|
||||
}
|
||||
|
||||
mw := &rewriteTarget{
|
||||
next: next,
|
||||
replacement: strings.TrimSpace(config.Replacement),
|
||||
xForwardedPrefix: config.XForwardedPrefix,
|
||||
name: name,
|
||||
}
|
||||
|
||||
if config.Regex != "" {
|
||||
exp, err := regexp.Compile(strings.TrimSpace(config.Regex))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compiling regular expression %s: %w", config.Regex, err)
|
||||
}
|
||||
mw.regexp = exp
|
||||
}
|
||||
|
||||
return mw, nil
|
||||
}
|
||||
|
||||
func (rt *rewriteTarget) GetTracingInformation() (string, string) {
|
||||
return rt.name, typeName
|
||||
}
|
||||
|
||||
func (rt *rewriteTarget) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
currentPath := req.URL.RawPath
|
||||
if currentPath == "" {
|
||||
currentPath = req.URL.EscapedPath()
|
||||
}
|
||||
|
||||
if rt.regexp != nil {
|
||||
if !rt.regexp.MatchString(currentPath) {
|
||||
rt.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
req.URL.RawPath = rt.regexp.ReplaceAllString(currentPath, rt.replacement)
|
||||
} else {
|
||||
req.URL.RawPath = rt.replacement
|
||||
}
|
||||
|
||||
if rt.xForwardedPrefix != "" {
|
||||
prefix := rt.xForwardedPrefix
|
||||
if rt.regexp != nil {
|
||||
prefix = rt.regexp.ReplaceAllString(currentPath, rt.xForwardedPrefix)
|
||||
}
|
||||
req.Header.Set(xForwardedPrefixHeader, prefix)
|
||||
}
|
||||
|
||||
// as replacement can introduce escaped characters
|
||||
// Path must remain an unescaped version of RawPath
|
||||
// Doesn't handle multiple times encoded replacement (`/` => `%2F` => `%252F` => ...)
|
||||
var err error
|
||||
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
|
||||
if err != nil {
|
||||
middlewares.GetLogger(context.Background(), rt.name, typeName).Error().Msgf("Unable to unescape url raw path %q: %v", req.URL.RawPath, err)
|
||||
observability.SetStatusErrorf(req.Context(), "Unable to unescape url raw path %q: %v", req.URL.RawPath, err)
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req.RequestURI = req.URL.RequestURI()
|
||||
|
||||
rt.next.ServeHTTP(rw, req)
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package rewritetarget
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
)
|
||||
|
||||
func TestRewriteTarget(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
path string
|
||||
config dynamic.RewriteTarget
|
||||
expectedPath string
|
||||
expectedRawPath string
|
||||
expectedXForwardedPrefix string
|
||||
expectsError bool
|
||||
}{
|
||||
{
|
||||
desc: "empty replacement",
|
||||
config: dynamic.RewriteTarget{
|
||||
Replacement: "",
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
desc: "plain replacement",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Replacement: "/replacement",
|
||||
},
|
||||
expectedPath: "/replacement",
|
||||
expectedRawPath: "/replacement",
|
||||
},
|
||||
{
|
||||
desc: "plain replacement with escaped char in replacement",
|
||||
path: "/foo",
|
||||
config: dynamic.RewriteTarget{
|
||||
Replacement: "/foo%2Fbar",
|
||||
},
|
||||
expectedPath: "/foo/bar",
|
||||
expectedRawPath: "/foo%2Fbar",
|
||||
},
|
||||
{
|
||||
desc: "plain replacement with x-forwarded-prefix",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Replacement: "/replacement",
|
||||
XForwardedPrefix: "/foo",
|
||||
},
|
||||
expectedPath: "/replacement",
|
||||
expectedRawPath: "/replacement",
|
||||
expectedXForwardedPrefix: "/foo",
|
||||
},
|
||||
{
|
||||
desc: "regex with capture group",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^/foo/(.*)`,
|
||||
Replacement: "/new/$1",
|
||||
},
|
||||
expectedPath: "/new/bar",
|
||||
expectedRawPath: "/new/bar",
|
||||
},
|
||||
{
|
||||
desc: "regex with multiple capture groups",
|
||||
path: "/downloads/src/source.go",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^(?i)/downloads/([^/]+)/([^/]+)$`,
|
||||
Replacement: "/downloads/$1-$2",
|
||||
},
|
||||
expectedPath: "/downloads/src-source.go",
|
||||
expectedRawPath: "/downloads/src-source.go",
|
||||
},
|
||||
{
|
||||
desc: "regex with escaped char in replacement",
|
||||
path: "/aaa/bbb",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `/aaa/bbb`,
|
||||
Replacement: "/foo%2Fbar",
|
||||
},
|
||||
expectedPath: "/foo/bar",
|
||||
expectedRawPath: "/foo%2Fbar",
|
||||
},
|
||||
{
|
||||
desc: "regex - no match passthrough",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^/baz/(.*)`,
|
||||
Replacement: "/new/$1",
|
||||
},
|
||||
expectedPath: "/foo/bar",
|
||||
expectedRawPath: "",
|
||||
},
|
||||
{
|
||||
desc: "invalid regex",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^(?err)/invalid/regexp/([^/]+)$`,
|
||||
Replacement: "/valid/$1",
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
desc: "regex with x-forwarded-prefix capture group",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^(/foo)/(.*)`,
|
||||
Replacement: "/$2",
|
||||
XForwardedPrefix: "$1",
|
||||
},
|
||||
expectedPath: "/bar",
|
||||
expectedRawPath: "/bar",
|
||||
expectedXForwardedPrefix: "/foo",
|
||||
},
|
||||
{
|
||||
desc: "regex with x-forwarded-prefix using third capture group",
|
||||
path: "/prefix/sub/endpoint",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^/(prefix)/(sub)/(.*)`,
|
||||
Replacement: "/$3",
|
||||
XForwardedPrefix: "/$1/$2",
|
||||
},
|
||||
expectedPath: "/endpoint",
|
||||
expectedRawPath: "/endpoint",
|
||||
expectedXForwardedPrefix: "/prefix/sub",
|
||||
},
|
||||
{
|
||||
desc: "x-forwarded-prefix not set when regex does not match",
|
||||
path: "/foo/bar",
|
||||
config: dynamic.RewriteTarget{
|
||||
Regex: `^/baz/(.*)`,
|
||||
Replacement: "/$1",
|
||||
XForwardedPrefix: "/baz",
|
||||
},
|
||||
expectedPath: "/foo/bar",
|
||||
expectedRawPath: "",
|
||||
expectedXForwardedPrefix: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
var actualPath, actualRawPath, actualXForwardedPrefix, requestURI string
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
actualPath = r.URL.Path
|
||||
actualRawPath = r.URL.RawPath
|
||||
actualXForwardedPrefix = r.Header.Get(xForwardedPrefixHeader)
|
||||
requestURI = r.RequestURI
|
||||
})
|
||||
|
||||
handler, err := New(t.Context(), next, test.config, "test-rewrite-target")
|
||||
if test.expectsError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
resp, err := http.Get(server.URL + test.path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
|
||||
assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
|
||||
assert.Equal(t, test.expectedXForwardedPrefix, actualXForwardedPrefix, "Unexpected %s header.", xForwardedPrefixHeader)
|
||||
|
||||
if actualRawPath == "" {
|
||||
assert.Equal(t, actualPath, requestURI, "Unexpected request URI.")
|
||||
} else {
|
||||
assert.Equal(t, actualRawPath, requestURI, "Unexpected request URI.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -83,8 +83,9 @@ type ingressConfig struct {
|
||||
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
|
||||
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
|
||||
|
||||
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
|
||||
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
|
||||
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
|
||||
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
|
||||
XForwardedPrefix *string `annotation:"nginx.ingress.kubernetes.io/x-forwarded-prefix"`
|
||||
|
||||
CustomHTTPErrors *[]string `annotation:"nginx.ingress.kubernetes.io/custom-http-errors"`
|
||||
DefaultBackend *string `annotation:"nginx.ingress.kubernetes.io/default-backend"`
|
||||
|
||||
@ -38,6 +38,8 @@ func Test_parseIngressConfig(t *testing.T) {
|
||||
"nginx.ingress.kubernetes.io/proxy-buffers-number": "8",
|
||||
"nginx.ingress.kubernetes.io/proxy-max-temp-file-size": "100m",
|
||||
"nginx.ingress.kubernetes.io/limit-rpm": "120",
|
||||
"nginx.ingress.kubernetes.io/x-forwarded-prefix": "/test",
|
||||
"nginx.ingress.kubernetes.io/upstream-vhost": "upstream-vhost",
|
||||
},
|
||||
expected: ingressConfig{
|
||||
SSLPassthrough: ptr.To(true),
|
||||
@ -61,6 +63,8 @@ func Test_parseIngressConfig(t *testing.T) {
|
||||
ProxyBuffersNumber: ptr.To(8),
|
||||
ProxyMaxTempFileSize: ptr.To("100m"),
|
||||
LimitRPM: ptr.To(120),
|
||||
XForwardedPrefix: ptr.To("/test"),
|
||||
UpstreamVhost: ptr.To("upstream-vhost"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-x-forwarded-prefix-no-rewrite-target
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/x-forwarded-prefix: "x-forwarded-prefix-header-value"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: x-forwarded-prefix.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
@ -0,0 +1,71 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-x-forwarded-prefix
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/x-forwarded-prefix: "x-forwarded-prefix-header-value"
|
||||
nginx.ingress.kubernetes.io/rewrite-target: "/path"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: x-forwarded-prefix.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-x-forwarded-prefix-regex
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
nginx.ingress.kubernetes.io/rewrite-target: "$2"
|
||||
nginx.ingress.kubernetes.io/x-forwarded-prefix: "$1"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: x-forwarded-prefix-regex.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: (/something)(/.+)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-x-forwarded-prefix-three-groups
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
nginx.ingress.kubernetes.io/rewrite-target: "/$3"
|
||||
nginx.ingress.kubernetes.io/x-forwarded-prefix: "/$1/$2"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: x-forwarded-prefix-three-groups.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /(prefix)/(sub)/(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
@ -1433,28 +1433,41 @@ func (p *Provider) applyCustomHeaders(routerName string, ingressConfig ingressCo
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validation identical to ingress-nginx.
|
||||
var regexPathWithCapture = regexp.MustCompile(`^/?[-._~a-zA-Z0-9/$:]*$`)
|
||||
|
||||
func applyRewriteTargetConfiguration(rulePath, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
|
||||
if ingressConfig.RewriteTarget == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip rewrite if the path is equal to the target.
|
||||
if *ingressConfig.RewriteTarget == rulePath {
|
||||
return
|
||||
}
|
||||
|
||||
rewriteTargetMiddlewareName := routerName + "-rewrite-target"
|
||||
|
||||
rewriteTarget := &dynamic.RewriteTarget{
|
||||
Replacement: *ingressConfig.RewriteTarget,
|
||||
}
|
||||
|
||||
if ptr.Deref(ingressConfig.UseRegex, false) {
|
||||
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
|
||||
ReplacePathRegex: &dynamic.ReplacePathRegex{
|
||||
Regex: rulePath,
|
||||
Replacement: *ingressConfig.RewriteTarget,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
|
||||
ReplacePath: &dynamic.ReplacePath{
|
||||
Path: *ingressConfig.RewriteTarget,
|
||||
},
|
||||
rewriteTarget.Regex = rulePath
|
||||
}
|
||||
|
||||
if ingressConfig.XForwardedPrefix != nil {
|
||||
if !regexPathWithCapture.MatchString(*ingressConfig.XForwardedPrefix) {
|
||||
log.Error().Msgf("Invalid x-forwarded-prefix value %q for router %q, skipping x-forwarded-prefix configuration", *ingressConfig.XForwardedPrefix, routerName)
|
||||
} else {
|
||||
rewriteTarget.XForwardedPrefix = *ingressConfig.XForwardedPrefix
|
||||
}
|
||||
}
|
||||
|
||||
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
|
||||
RewriteTarget: rewriteTarget,
|
||||
}
|
||||
|
||||
rt.Middlewares = append(rt.Middlewares, rewriteTargetMiddlewareName)
|
||||
}
|
||||
|
||||
|
||||
@ -1157,6 +1157,224 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "X-forwarded-prefix with missing rewrite-target",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-x-forwarded-prefix-no-rewrite-target.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0": {
|
||||
Rule: "Host(`x-forwarded-prefix.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0-retry"},
|
||||
Service: "default-ingress-with-x-forwarded-prefix-no-rewrite-target-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-ingress-with-x-forwarded-prefix-no-rewrite-target",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-ingress-with-x-forwarded-prefix-no-rewrite-target": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "X-forwarded-prefix",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-x-forwarded-prefix.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-ingress-with-x-forwarded-prefix-rule-0-path-0": {
|
||||
Rule: "Host(`x-forwarded-prefix.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-rule-0-path-0-retry"},
|
||||
Service: "default-ingress-with-x-forwarded-prefix-whoami-80",
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0": {
|
||||
Rule: "Host(`x-forwarded-prefix-regex.localhost`) && PathRegexp(`^(/something)(/.+)`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-retry"},
|
||||
Service: "default-ingress-with-x-forwarded-prefix-regex-whoami-80",
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0": {
|
||||
Rule: "Host(`x-forwarded-prefix-three-groups.localhost`) && PathRegexp(`^/(prefix)/(sub)/(.*)`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-retry"},
|
||||
Service: "default-ingress-with-x-forwarded-prefix-three-groups-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-rewrite-target": {
|
||||
RewriteTarget: &dynamic.RewriteTarget{
|
||||
Regex: "/(prefix)/(sub)/(.*)",
|
||||
Replacement: "/$3",
|
||||
XForwardedPrefix: "/$1/$2",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-rule-0-path-0-rewrite-target": {
|
||||
RewriteTarget: &dynamic.RewriteTarget{
|
||||
Replacement: "/path",
|
||||
XForwardedPrefix: "x-forwarded-prefix-header-value",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-rewrite-target": {
|
||||
RewriteTarget: &dynamic.RewriteTarget{
|
||||
Regex: "(/something)(/.+)",
|
||||
Replacement: "$2",
|
||||
XForwardedPrefix: "$1",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{
|
||||
Attempts: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-x-forwarded-prefix-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-ingress-with-x-forwarded-prefix",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-three-groups-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-ingress-with-x-forwarded-prefix-three-groups",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-regex-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-ingress-with-x-forwarded-prefix-regex",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-ingress-with-x-forwarded-prefix": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-three-groups": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
"default-ingress-with-x-forwarded-prefix-regex": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Use Regex",
|
||||
paths: []string{
|
||||
@ -1242,7 +1460,7 @@ func TestLoadIngresses(t *testing.T) {
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-rewrite-target-rule-0-path-0-rewrite-target": {
|
||||
ReplacePathRegex: &dynamic.ReplacePathRegex{
|
||||
RewriteTarget: &dynamic.RewriteTarget{
|
||||
Regex: "/something(/|$)(.*)",
|
||||
Replacement: "/$2",
|
||||
},
|
||||
@ -1313,8 +1531,8 @@ func TestLoadIngresses(t *testing.T) {
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-rewrite-target": {
|
||||
ReplacePath: &dynamic.ReplacePath{
|
||||
Path: "/rewritten",
|
||||
RewriteTarget: &dynamic.RewriteTarget{
|
||||
Replacement: "/rewritten",
|
||||
},
|
||||
},
|
||||
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-retry": {
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/rewritetarget"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/snippet"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/ipwhitelist"
|
||||
@ -340,6 +341,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
|
||||
}
|
||||
}
|
||||
|
||||
// RewriteTarget
|
||||
if config.RewriteTarget != nil {
|
||||
if middleware != nil {
|
||||
return nil, badConf
|
||||
}
|
||||
middleware = func(next http.Handler) (http.Handler, error) {
|
||||
return rewritetarget.New(ctx, next, *config.RewriteTarget, middlewareName)
|
||||
}
|
||||
}
|
||||
|
||||
// Retry
|
||||
if config.Retry != nil {
|
||||
if middleware != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user