Fix HTTPRoute Redirect Filter with port and scheme
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
parent
7eac92f49c
commit
28d40e7f3c
13 changed files with 431 additions and 93 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
134
pkg/middlewares/gatewayapi/redirect/request_redirect.go
Normal file
134
pkg/middlewares/gatewayapi/redirect/request_redirect.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
203
pkg/middlewares/gatewayapi/redirect/request_redirect_test.go
Normal file
203
pkg/middlewares/gatewayapi/redirect/request_redirect_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<userInfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
|
||||
RequestRedirect: &dynamic.RequestRedirect{
|
||||
Regex: `^(?P<scheme>[a-z]+):\/\/(?P<userinfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
|
||||
Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port),
|
||||
Permanent: statusCode == http.StatusMovedPermanently,
|
||||
},
|
||||
|
|
|
@ -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<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||
Replacement: "https://${userinfo}${hostname}${port}/${path}",
|
||||
RequestRedirect: &dynamic.RequestRedirect{
|
||||
Regex: "^(?P<scheme>[a-z]+):\\/\\/(?P<userinfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||
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<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||
Replacement: "http://${userinfo}example.com:443/${path}",
|
||||
RequestRedirect: &dynamic.RequestRedirect{
|
||||
Regex: "^(?P<scheme>[a-z]+):\\/\\/(?P<userinfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||
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{},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue