From 28d40e7f3c189790ef7a635dbe46ec3620f1b999 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 6 Jun 2024 10:56:03 +0200 Subject: [PATCH] Fix HTTPRoute Redirect Filter with port and scheme Co-authored-by: Kevin Pollet --- .../fixtures/k8s-conformance/02-traefik.yml | 15 +- integration/k8s_conformance_test.go | 29 ++- pkg/config/dynamic/middlewares.go | 10 + pkg/config/dynamic/zz_generated.deepcopy.go | 21 ++ .../headermodifier/request_header_modifier.go | 0 .../request_header_modifier_test.go | 0 .../gatewayapi/redirect/request_redirect.go | 134 ++++++++++++ .../redirect/request_redirect_test.go | 203 ++++++++++++++++++ .../httproute/filter_http_to_https.yml | 8 +- ...r_http_to_https_with_hostname_and_port.yml | 8 +- pkg/provider/kubernetes/gateway/httproute.go | 22 +- .../kubernetes/gateway/kubernetes_test.go | 62 +----- pkg/server/middleware/middlewares.go | 12 +- 13 files changed, 431 insertions(+), 93 deletions(-) rename pkg/middlewares/{ => gatewayapi}/headermodifier/request_header_modifier.go (100%) rename pkg/middlewares/{ => gatewayapi}/headermodifier/request_header_modifier_test.go (100%) create mode 100644 pkg/middlewares/gatewayapi/redirect/request_redirect.go create mode 100644 pkg/middlewares/gatewayapi/redirect/request_redirect_test.go diff --git a/integration/fixtures/k8s-conformance/02-traefik.yml b/integration/fixtures/k8s-conformance/02-traefik.yml index becc6dc5c..6f780affb 100644 --- a/integration/fixtures/k8s-conformance/02-traefik.yml +++ b/integration/fixtures/k8s-conformance/02-traefik.yml @@ -48,6 +48,8 @@ spec: - --api.insecure - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 + - --entrypoints.web8080.address=:8080 + - --entrypoints.traefik.address=:9000 - --experimental.kubernetesgateway - --providers.kubernetesgateway.experimentalChannel - --providers.kubernetesgateway.statusaddress.service.namespace=traefik @@ -55,10 +57,12 @@ spec: ports: - name: web containerPort: 80 - - name: admin - containerPort: 8080 - name: websecure containerPort: 443 + - name: web8080 + containerPort: 8080 + - name: traefik + containerPort: 9000 --- apiVersion: v1 @@ -78,5 +82,8 @@ spec: name: websecure targetPort: websecure - port: 8080 - name: admin - targetPort: admin + name: web8080 + targetPort: web8080 + - port: 9000 + name: traefik + targetPort: traefik diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index 5e318d562..afae520fc 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -166,7 +166,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { k3sContainerIP, err := s.k3sContainer.ContainerIP(context.Background()) require.NoError(s.T(), err) - err = try.GetRequest("http://"+k3sContainerIP+":8080/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`)) + err = try.GetRequest("http://"+k3sContainerIP+":9000/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`)) require.NoError(s.T(), err) opts := ksuite.Options{ @@ -195,23 +195,32 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { LatestObservedGenerationSet: 5 * time.Second, RequiredConsecutiveSuccesses: 0, }, - SupportedFeatures: sets.New(ksuite.SupportGateway, ksuite.SupportHTTPRoute). - Union(ksuite.HTTPRouteExtendedFeatures), + SupportedFeatures: sets.New(ksuite.SupportGateway, + ksuite.SupportGatewayPort8080, + ksuite.SupportHTTPRoute, + ksuite.SupportHTTPRouteQueryParamMatching, + ksuite.SupportHTTPRouteMethodMatching, + ksuite.SupportHTTPRoutePortRedirect, + ksuite.SupportHTTPRouteSchemeRedirect, + ksuite.SupportHTTPRouteHostRewrite, + ksuite.SupportHTTPRoutePathRewrite, + ), + ExemptFeatures: sets.New( + ksuite.SupportHTTPRouteRequestTimeout, + ksuite.SupportHTTPRouteBackendTimeout, + ksuite.SupportHTTPRouteResponseHeaderModification, + ksuite.SupportHTTPRoutePathRedirect, + ksuite.SupportHTTPRouteRequestMirror, + ksuite.SupportHTTPRouteRequestMultipleMirrors, + ), EnableAllSupportedFeatures: false, RunTest: *k8sConformanceRunTest, // Until the feature are all supported, following tests are skipped. SkipTests: []string{ tests.HTTPRouteMethodMatching.ShortName, tests.HTTPRouteQueryParamMatching.ShortName, - tests.HTTPRouteRedirectPath.ShortName, - tests.HTTPRouteRedirectPortAndScheme.ShortName, - tests.HTTPRouteRequestMirror.ShortName, - tests.HTTPRouteRequestMultipleMirrors.ShortName, - tests.HTTPRouteResponseHeaderModifier.ShortName, tests.HTTPRouteRewriteHost.ShortName, tests.HTTPRouteRewritePath.ShortName, - tests.HTTPRouteTimeoutBackendRequest.ShortName, - tests.HTTPRouteTimeoutRequest.ShortName, }, } diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 59c91814b..e2c9ed177 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -42,6 +42,7 @@ type Middleware struct { // Gateway API HTTPRoute filters middlewares. RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -685,3 +686,12 @@ type RequestHeaderModifier struct { Add map[string]string `json:"add,omitempty"` Remove []string `json:"remove,omitempty"` } + +// +k8s:deepcopy-gen=true + +// RequestRedirect holds the request redirect middleware configuration. +type RequestRedirect struct { + Regex string `json:"regex,omitempty"` + Replacement string `json:"replacement,omitempty"` + Permanent bool `json:"permanent,omitempty"` +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 05436088b..fb496fe66 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -864,6 +864,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { *out = new(RequestHeaderModifier) (*in).DeepCopyInto(*out) } + if in.RequestRedirect != nil { + in, out := &in.RequestRedirect, &out.RequestRedirect + *out = new(RequestRedirect) + **out = **in + } return } @@ -1107,6 +1112,22 @@ func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestRedirect. +func (in *RequestRedirect) DeepCopy() *RequestRedirect { + if in == nil { + return nil + } + out := new(RequestRedirect) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) { *out = *in diff --git a/pkg/middlewares/headermodifier/request_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go similarity index 100% rename from pkg/middlewares/headermodifier/request_header_modifier.go rename to pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go diff --git a/pkg/middlewares/headermodifier/request_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go similarity index 100% rename from pkg/middlewares/headermodifier/request_header_modifier_test.go rename to pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect.go b/pkg/middlewares/gatewayapi/redirect/request_redirect.go new file mode 100644 index 000000000..d1dd984ba --- /dev/null +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect.go @@ -0,0 +1,134 @@ +package redirect + +import ( + "context" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/middlewares" + "github.com/vulcand/oxy/v2/utils" + "go.opentelemetry.io/otel/trace" +) + +const ( + schemeHTTP = "http" + schemeHTTPS = "https" + typeName = "RequestRedirect" +) + +var uriRegexp = regexp.MustCompile(`^(https?):\/\/(\[[\w:.]+\]|[\w\._-]+)?(:\d+)?(.*)$`) + +// NewRequestRedirect creates a redirect middleware. +func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.RequestRedirect, name string) (http.Handler, error) { + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + logger.Debug().Msgf("Setting up redirection from %s to %s", conf.Regex, conf.Replacement) + + re, err := regexp.Compile(conf.Regex) + if err != nil { + return nil, err + } + + return &redirect{ + regex: re, + replacement: conf.Replacement, + permanent: conf.Permanent, + errHandler: utils.DefaultHandler, + next: next, + name: name, + rawURL: rawURL, + }, nil +} + +type redirect struct { + next http.Handler + regex *regexp.Regexp + replacement string + permanent bool + errHandler utils.ErrorHandler + name string + rawURL func(*http.Request) string +} + +func rawURL(req *http.Request) string { + scheme := schemeHTTP + host := req.Host + port := "" + uri := req.RequestURI + + if match := uriRegexp.FindStringSubmatch(req.RequestURI); len(match) > 0 { + scheme = match[1] + + if len(match[2]) > 0 { + host = match[2] + } + + if len(match[3]) > 0 { + port = match[3] + } + + uri = match[4] + } + + if req.TLS != nil { + scheme = schemeHTTPS + } + + return strings.Join([]string{scheme, "://", host, port, uri}, "") +} + +func (r *redirect) GetTracingInformation() (string, string, trace.SpanKind) { + return r.name, typeName, trace.SpanKindInternal +} + +func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + oldURL := r.rawURL(req) + + // If the Regexp doesn't match, skip to the next handler. + if !r.regex.MatchString(oldURL) { + r.next.ServeHTTP(rw, req) + return + } + + // Apply a rewrite regexp to the URL. + newURL := r.regex.ReplaceAllString(oldURL, r.replacement) + + // Parse the rewritten URL and replace request URL with it. + parsedURL, err := url.Parse(newURL) + if err != nil { + r.errHandler.ServeHTTP(rw, req, err) + return + } + + handler := &moveHandler{location: parsedURL, permanent: r.permanent} + handler.ServeHTTP(rw, req) +} + +type moveHandler struct { + location *url.URL + permanent bool +} + +func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Location", m.location.String()) + + status := http.StatusFound + if req.Method != http.MethodGet { + status = http.StatusTemporaryRedirect + } + + if m.permanent { + status = http.StatusMovedPermanently + if req.Method != http.MethodGet { + status = http.StatusPermanentRedirect + } + } + rw.WriteHeader(status) + _, err := rw.Write([]byte(http.StatusText(status))) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + } +} diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go b/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go new file mode 100644 index 000000000..6fe9050a5 --- /dev/null +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go @@ -0,0 +1,203 @@ +package redirect + +import ( + "context" + "crypto/tls" + "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 TestRequestRedirectHandler(t *testing.T) { + testCases := []struct { + desc string + config dynamic.RequestRedirect + method string + url string + headers map[string]string + secured bool + expectedURL string + expectedStatus int + errorExpected bool + }{ + { + desc: "simple redirection", + config: dynamic.RequestRedirect{ + Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`, + Replacement: "https://${1}bar$2:443$4", + }, + url: "http://foo.com:80", + expectedURL: "https://foobar.com:443", + expectedStatus: http.StatusFound, + }, + { + desc: "URL doesn't match regex", + config: dynamic.RequestRedirect{ + Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`, + Replacement: "https://${1}bar$2:443$4", + }, + url: "http://bar.com:80", + expectedStatus: http.StatusOK, + }, + { + desc: "invalid rewritten URL", + config: dynamic.RequestRedirect{ + Regex: `^(.*)$`, + Replacement: "http://192.168.0.%31/", + }, + url: "http://foo.com:80", + expectedStatus: http.StatusBadGateway, + }, + { + desc: "invalid regex", + config: dynamic.RequestRedirect{ + Regex: `^(.*`, + Replacement: "$1", + }, + url: "http://foo.com:80", + errorExpected: true, + }, + { + desc: "HTTP to HTTPS permanent", + config: dynamic.RequestRedirect{ + Regex: `^http://`, + Replacement: "https://$1", + Permanent: true, + }, + url: "http://foo", + expectedURL: "https://foo", + expectedStatus: http.StatusMovedPermanently, + }, + { + desc: "HTTPS to HTTP permanent", + config: dynamic.RequestRedirect{ + Regex: `https://foo`, + Replacement: "http://foo", + Permanent: true, + }, + secured: true, + url: "https://foo", + expectedURL: "http://foo", + expectedStatus: http.StatusMovedPermanently, + }, + { + desc: "HTTP to HTTPS", + config: dynamic.RequestRedirect{ + Regex: `http://foo:80`, + Replacement: "https://foo:443", + }, + url: "http://foo:80", + expectedURL: "https://foo:443", + expectedStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTPS, with X-Forwarded-Proto", + config: dynamic.RequestRedirect{ + Regex: `http://foo:80`, + Replacement: "https://foo:443", + }, + url: "http://foo:80", + headers: map[string]string{ + "X-Forwarded-Proto": "https", + }, + expectedURL: "https://foo:443", + expectedStatus: http.StatusFound, + }, + { + desc: "HTTPS to HTTP", + config: dynamic.RequestRedirect{ + Regex: `https://foo:443`, + Replacement: "http://foo:80", + }, + secured: true, + url: "https://foo:443", + expectedURL: "http://foo:80", + expectedStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTP", + config: dynamic.RequestRedirect{ + Regex: `http://foo:80`, + Replacement: "http://foo:88", + }, + url: "http://foo:80", + expectedURL: "http://foo:88", + expectedStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTP POST", + config: dynamic.RequestRedirect{ + Regex: `^http://`, + Replacement: "https://$1", + }, + url: "http://foo", + method: http.MethodPost, + expectedURL: "https://foo", + expectedStatus: http.StatusTemporaryRedirect, + }, + { + desc: "HTTP to HTTP POST permanent", + config: dynamic.RequestRedirect{ + Regex: `^http://`, + Replacement: "https://$1", + Permanent: true, + }, + url: "http://foo", + method: http.MethodPost, + expectedURL: "https://foo", + expectedStatus: http.StatusPermanentRedirect, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + handler, err := NewRequestRedirect(context.Background(), next, test.config, "traefikTest") + + if test.errorExpected { + require.Error(t, err) + require.Nil(t, handler) + } else { + require.NoError(t, err) + require.NotNil(t, handler) + + recorder := httptest.NewRecorder() + + method := http.MethodGet + if test.method != "" { + method = test.method + } + + req := httptest.NewRequest(method, test.url, nil) + if test.secured { + req.TLS = &tls.ConnectionState{} + } + + for k, v := range test.headers { + req.Header.Set(k, v) + } + + req.Header.Set("X-Foo", "bar") + handler.ServeHTTP(recorder, req) + + assert.Equal(t, test.expectedStatus, recorder.Code) + switch test.expectedStatus { + case http.StatusMovedPermanently, http.StatusFound, http.StatusTemporaryRedirect, http.StatusPermanentRedirect: + location, err := recorder.Result().Location() + require.NoError(t, err) + + assert.Equal(t, test.expectedURL, location.String()) + default: + location, err := recorder.Result().Location() + require.Errorf(t, err, "Location %v", location) + } + } + }) + } +} diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml index de4cdf9ec..dda03362e 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml @@ -39,13 +39,7 @@ spec: hostnames: - "example.org" rules: - - backendRefs: - - name: whoami - port: 80 - weight: 1 - kind: Service - group: "" - filters: + - filters: - type: RequestRedirect requestRedirect: scheme: https diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml index f1aba4c62..971a0ae81 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml @@ -39,13 +39,7 @@ spec: hostnames: - "example.org" rules: - - backendRefs: - - name: whoami - port: 80 - weight: 1 - kind: Service - group: "" - filters: + - filters: - type: RequestRedirect requestRedirect: hostname: example.com diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 1570a55ea..bc5d67160 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -135,7 +135,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener ga var wrr dynamic.WeightedRoundRobin wrrName := provider.Normalize(routerKey + "-wrr") - middlewares, err := p.loadMiddlewares(listener.Protocol, route.Namespace, routerKey, routeRule.Filters) + middlewares, err := p.loadMiddlewares(route.Namespace, routerKey, routeRule.Filters) if err != nil { log.Ctx(ctx).Error(). Err(err). @@ -294,14 +294,14 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa return backendFunc(string(backendRef.Name), namespace) } -func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) { +func (p *Provider) loadMiddlewares(namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) { middlewares := make(map[string]*dynamic.Middleware) for i, filter := range filters { switch filter.Type { case gatev1.HTTPRouteFilterRequestRedirect: middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) - middlewares[middlewareName] = createRedirectRegexMiddleware(listenerProtocol, filter.RequestRedirect) + middlewares[middlewareName] = createRedirectMiddleware(filter.RequestRedirect) case gatev1.HTTPRouteFilterRequestHeaderModifier: middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) @@ -573,25 +573,27 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl } } -func createRedirectRegexMiddleware(listenerProtocol gatev1.ProtocolType, filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware { - // The spec allows for an empty string in which case we should use the - // scheme of the request which in this case is the listener scheme. - filterScheme := ptr.Deref(filter.Scheme, strings.ToLower(string(listenerProtocol))) - statusCode := ptr.Deref(filter.StatusCode, http.StatusFound) +func createRedirectMiddleware(filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware { + filterScheme := ptr.Deref(filter.Scheme, "${scheme}") port := "${port}" + if filterScheme == "http" || filterScheme == "https" { + port = "" + } if filter.Port != nil { port = fmt.Sprintf(":%d", *filter.Port) } + statusCode := ptr.Deref(filter.StatusCode, http.StatusFound) + hostname := "${hostname}" if filter.Hostname != nil && *filter.Hostname != "" { hostname = string(*filter.Hostname) } return &dynamic.Middleware{ - RedirectRegex: &dynamic.RedirectRegex{ - Regex: `^[a-z]+:\/\/(?P.+@)?(?P\[[\w:\.]+\]|[\w\._-]+)(?P:\d+)?\/(?P.*)`, + RequestRedirect: &dynamic.RequestRedirect{ + Regex: `^(?P[a-z]+):\/\/(?P.+@)?(?P\[[\w:\.]+\]|[\w\._-]+)(?P:\d+)?\/(?P.*)`, Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port), Permanent: statusCode == http.StatusMovedPermanently, }, diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 8a4ce02b5..94bb5fe6b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1669,39 +1669,16 @@ func TestLoadHTTPRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": { - RedirectRegex: &dynamic.RedirectRegex{ - Regex: "^[a-z]+:\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", - Replacement: "https://${userinfo}${hostname}${port}/${path}", + RequestRedirect: &dynamic.RequestRedirect{ + Regex: "^(?P[a-z]+):\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", + Replacement: "https://${userinfo}${hostname}/${path}", Permanent: true, }, }, }, Services: map[string]*dynamic.Service{ "default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": { - Weighted: &dynamic.WeightedRoundRobin{ - Services: []dynamic.WRRService{ - { - Name: "default-whoami-80", - Weight: ptr.To(1), - }, - }, - }, - }, - "default-whoami-80": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: "http://10.10.0.1:80", - }, - { - URL: "http://10.10.0.2:80", - }, - }, - PassHostHeader: ptr.To(true), - ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: ptypes.Duration(100 * time.Millisecond), - }, - }, + Weighted: &dynamic.WeightedRoundRobin{}, }, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -1739,38 +1716,15 @@ func TestLoadHTTPRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": { - RedirectRegex: &dynamic.RedirectRegex{ - Regex: "^[a-z]+:\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", - Replacement: "http://${userinfo}example.com:443/${path}", + RequestRedirect: &dynamic.RequestRedirect{ + Regex: "^(?P[a-z]+):\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", + Replacement: "${scheme}://${userinfo}example.com:443/${path}", }, }, }, Services: map[string]*dynamic.Service{ "default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": { - Weighted: &dynamic.WeightedRoundRobin{ - Services: []dynamic.WRRService{ - { - Name: "default-whoami-80", - Weight: ptr.To(1), - }, - }, - }, - }, - "default-whoami-80": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: "http://10.10.0.1:80", - }, - { - URL: "http://10.10.0.2:80", - }, - }, - PassHostHeader: ptr.To(true), - ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: ptypes.Duration(100 * time.Millisecond), - }, - }, + Weighted: &dynamic.WeightedRoundRobin{}, }, }, ServersTransports: map[string]*dynamic.ServersTransport{}, diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 2217dc58a..b5d0447d8 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -20,8 +20,9 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/compress" "github.com/traefik/traefik/v3/pkg/middlewares/contenttype" "github.com/traefik/traefik/v3/pkg/middlewares/customerrors" + "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/headermodifier" + gapiredirect "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/redirect" "github.com/traefik/traefik/v3/pkg/middlewares/grpcweb" - "github.com/traefik/traefik/v3/pkg/middlewares/headermodifier" "github.com/traefik/traefik/v3/pkg/middlewares/headers" "github.com/traefik/traefik/v3/pkg/middlewares/inflightreq" "github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist" @@ -395,6 +396,15 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } + if config.RequestRedirect != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return gapiredirect.NewRequestRedirect(ctx, next, *config.RequestRedirect, middlewareName) + } + } + if middleware == nil { return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName) }