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
|
- --api.insecure
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
- --entrypoints.websecure.address=:443
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --entrypoints.web8080.address=:8080
|
||||||
|
- --entrypoints.traefik.address=:9000
|
||||||
- --experimental.kubernetesgateway
|
- --experimental.kubernetesgateway
|
||||||
- --providers.kubernetesgateway.experimentalChannel
|
- --providers.kubernetesgateway.experimentalChannel
|
||||||
- --providers.kubernetesgateway.statusaddress.service.namespace=traefik
|
- --providers.kubernetesgateway.statusaddress.service.namespace=traefik
|
||||||
|
@ -55,10 +57,12 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- name: web
|
- name: web
|
||||||
containerPort: 80
|
containerPort: 80
|
||||||
- name: admin
|
|
||||||
containerPort: 8080
|
|
||||||
- name: websecure
|
- name: websecure
|
||||||
containerPort: 443
|
containerPort: 443
|
||||||
|
- name: web8080
|
||||||
|
containerPort: 8080
|
||||||
|
- name: traefik
|
||||||
|
containerPort: 9000
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
@ -78,5 +82,8 @@ spec:
|
||||||
name: websecure
|
name: websecure
|
||||||
targetPort: websecure
|
targetPort: websecure
|
||||||
- port: 8080
|
- port: 8080
|
||||||
name: admin
|
name: web8080
|
||||||
targetPort: admin
|
targetPort: web8080
|
||||||
|
- port: 9000
|
||||||
|
name: traefik
|
||||||
|
targetPort: traefik
|
||||||
|
|
|
@ -166,7 +166,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
|
||||||
k3sContainerIP, err := s.k3sContainer.ContainerIP(context.Background())
|
k3sContainerIP, err := s.k3sContainer.ContainerIP(context.Background())
|
||||||
require.NoError(s.T(), err)
|
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)
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
opts := ksuite.Options{
|
opts := ksuite.Options{
|
||||||
|
@ -195,23 +195,32 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
|
||||||
LatestObservedGenerationSet: 5 * time.Second,
|
LatestObservedGenerationSet: 5 * time.Second,
|
||||||
RequiredConsecutiveSuccesses: 0,
|
RequiredConsecutiveSuccesses: 0,
|
||||||
},
|
},
|
||||||
SupportedFeatures: sets.New(ksuite.SupportGateway, ksuite.SupportHTTPRoute).
|
SupportedFeatures: sets.New(ksuite.SupportGateway,
|
||||||
Union(ksuite.HTTPRouteExtendedFeatures),
|
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,
|
EnableAllSupportedFeatures: false,
|
||||||
RunTest: *k8sConformanceRunTest,
|
RunTest: *k8sConformanceRunTest,
|
||||||
// Until the feature are all supported, following tests are skipped.
|
// Until the feature are all supported, following tests are skipped.
|
||||||
SkipTests: []string{
|
SkipTests: []string{
|
||||||
tests.HTTPRouteMethodMatching.ShortName,
|
tests.HTTPRouteMethodMatching.ShortName,
|
||||||
tests.HTTPRouteQueryParamMatching.ShortName,
|
tests.HTTPRouteQueryParamMatching.ShortName,
|
||||||
tests.HTTPRouteRedirectPath.ShortName,
|
|
||||||
tests.HTTPRouteRedirectPortAndScheme.ShortName,
|
|
||||||
tests.HTTPRouteRequestMirror.ShortName,
|
|
||||||
tests.HTTPRouteRequestMultipleMirrors.ShortName,
|
|
||||||
tests.HTTPRouteResponseHeaderModifier.ShortName,
|
|
||||||
tests.HTTPRouteRewriteHost.ShortName,
|
tests.HTTPRouteRewriteHost.ShortName,
|
||||||
tests.HTTPRouteRewritePath.ShortName,
|
tests.HTTPRouteRewritePath.ShortName,
|
||||||
tests.HTTPRouteTimeoutBackendRequest.ShortName,
|
|
||||||
tests.HTTPRouteTimeoutRequest.ShortName,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ type Middleware struct {
|
||||||
|
|
||||||
// Gateway API HTTPRoute filters middlewares.
|
// Gateway API HTTPRoute filters middlewares.
|
||||||
RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
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
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -685,3 +686,12 @@ type RequestHeaderModifier struct {
|
||||||
Add map[string]string `json:"add,omitempty"`
|
Add map[string]string `json:"add,omitempty"`
|
||||||
Remove []string `json:"remove,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)
|
*out = new(RequestHeaderModifier)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.RequestRedirect != nil {
|
||||||
|
in, out := &in.RequestRedirect, &out.RequestRedirect
|
||||||
|
*out = new(RequestRedirect)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1107,6 +1112,22 @@ func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
||||||
*out = *in
|
*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:
|
hostnames:
|
||||||
- "example.org"
|
- "example.org"
|
||||||
rules:
|
rules:
|
||||||
- backendRefs:
|
- filters:
|
||||||
- name: whoami
|
|
||||||
port: 80
|
|
||||||
weight: 1
|
|
||||||
kind: Service
|
|
||||||
group: ""
|
|
||||||
filters:
|
|
||||||
- type: RequestRedirect
|
- type: RequestRedirect
|
||||||
requestRedirect:
|
requestRedirect:
|
||||||
scheme: https
|
scheme: https
|
||||||
|
|
|
@ -39,13 +39,7 @@ spec:
|
||||||
hostnames:
|
hostnames:
|
||||||
- "example.org"
|
- "example.org"
|
||||||
rules:
|
rules:
|
||||||
- backendRefs:
|
- filters:
|
||||||
- name: whoami
|
|
||||||
port: 80
|
|
||||||
weight: 1
|
|
||||||
kind: Service
|
|
||||||
group: ""
|
|
||||||
filters:
|
|
||||||
- type: RequestRedirect
|
- type: RequestRedirect
|
||||||
requestRedirect:
|
requestRedirect:
|
||||||
hostname: example.com
|
hostname: example.com
|
||||||
|
|
|
@ -135,7 +135,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener ga
|
||||||
var wrr dynamic.WeightedRoundRobin
|
var wrr dynamic.WeightedRoundRobin
|
||||||
wrrName := provider.Normalize(routerKey + "-wrr")
|
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 {
|
if err != nil {
|
||||||
log.Ctx(ctx).Error().
|
log.Ctx(ctx).Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
|
@ -294,14 +294,14 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
|
||||||
return backendFunc(string(backendRef.Name), namespace)
|
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)
|
middlewares := make(map[string]*dynamic.Middleware)
|
||||||
|
|
||||||
for i, filter := range filters {
|
for i, filter := range filters {
|
||||||
switch filter.Type {
|
switch filter.Type {
|
||||||
case gatev1.HTTPRouteFilterRequestRedirect:
|
case gatev1.HTTPRouteFilterRequestRedirect:
|
||||||
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
|
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:
|
case gatev1.HTTPRouteFilterRequestHeaderModifier:
|
||||||
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
|
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 {
|
func createRedirectMiddleware(filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware {
|
||||||
// The spec allows for an empty string in which case we should use the
|
filterScheme := ptr.Deref(filter.Scheme, "${scheme}")
|
||||||
// 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)
|
|
||||||
|
|
||||||
port := "${port}"
|
port := "${port}"
|
||||||
|
if filterScheme == "http" || filterScheme == "https" {
|
||||||
|
port = ""
|
||||||
|
}
|
||||||
if filter.Port != nil {
|
if filter.Port != nil {
|
||||||
port = fmt.Sprintf(":%d", *filter.Port)
|
port = fmt.Sprintf(":%d", *filter.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusCode := ptr.Deref(filter.StatusCode, http.StatusFound)
|
||||||
|
|
||||||
hostname := "${hostname}"
|
hostname := "${hostname}"
|
||||||
if filter.Hostname != nil && *filter.Hostname != "" {
|
if filter.Hostname != nil && *filter.Hostname != "" {
|
||||||
hostname = string(*filter.Hostname)
|
hostname = string(*filter.Hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dynamic.Middleware{
|
return &dynamic.Middleware{
|
||||||
RedirectRegex: &dynamic.RedirectRegex{
|
RequestRedirect: &dynamic.RequestRedirect{
|
||||||
Regex: `^[a-z]+:\/\/(?P<userInfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
|
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),
|
Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port),
|
||||||
Permanent: statusCode == http.StatusMovedPermanently,
|
Permanent: statusCode == http.StatusMovedPermanently,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1669,39 +1669,16 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": {
|
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": {
|
||||||
RedirectRegex: &dynamic.RedirectRegex{
|
RequestRedirect: &dynamic.RequestRedirect{
|
||||||
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
Regex: "^(?P<scheme>[a-z]+):\\/\\/(?P<userinfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||||
Replacement: "https://${userinfo}${hostname}${port}/${path}",
|
Replacement: "https://${userinfo}${hostname}/${path}",
|
||||||
Permanent: true,
|
Permanent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": {
|
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": {
|
||||||
Weighted: &dynamic.WeightedRoundRobin{
|
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||||
|
@ -1739,38 +1716,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": {
|
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-requestredirect-0": {
|
||||||
RedirectRegex: &dynamic.RedirectRegex{
|
RequestRedirect: &dynamic.RequestRedirect{
|
||||||
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
Regex: "^(?P<scheme>[a-z]+):\\/\\/(?P<userinfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
|
||||||
Replacement: "http://${userinfo}example.com:443/${path}",
|
Replacement: "${scheme}://${userinfo}example.com:443/${path}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": {
|
"default-http-app-1-my-gateway-web-fa136e10345bd0e7248d-wrr": {
|
||||||
Weighted: &dynamic.WeightedRoundRobin{
|
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
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/compress"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
|
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
|
"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/grpcweb"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/headermodifier"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
|
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
|
"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 {
|
if middleware == nil {
|
||||||
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue