Add HTTPUrlRewrite Filter in Gateway API
This commit is contained in:
parent
3ca667a3d4
commit
a696f7c654
15 changed files with 754 additions and 110 deletions
|
@ -22,7 +22,7 @@ type requestHeaderModifier struct {
|
|||
}
|
||||
|
||||
// NewRequestHeaderModifier creates a new request header modifier middleware.
|
||||
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) (http.Handler, error) {
|
||||
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler {
|
||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
||||
logger.Debug().Msg("Creating middleware")
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
|
|||
set: config.Set,
|
||||
add: config.Add,
|
||||
remove: config.Remove,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||
)
|
||||
|
@ -104,8 +103,7 @@ func TestRequestHeaderModifier(t *testing.T) {
|
|||
gotHeaders = r.Header
|
||||
})
|
||||
|
||||
handler, err := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
|
||||
require.NoError(t, err)
|
||||
handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
|
||||
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
|
||||
if test.requestHeaders != nil {
|
||||
|
|
|
@ -18,8 +18,9 @@ const (
|
|||
)
|
||||
|
||||
type redirect struct {
|
||||
name string
|
||||
next http.Handler
|
||||
name string
|
||||
next http.Handler
|
||||
|
||||
scheme *string
|
||||
hostname *string
|
||||
port *string
|
||||
|
|
|
@ -2,7 +2,6 @@ package redirect
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
@ -15,15 +14,12 @@ import (
|
|||
|
||||
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 string
|
||||
config dynamic.RequestRedirect
|
||||
url string
|
||||
wantURL string
|
||||
wantStatus int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "wrong status code",
|
||||
|
@ -31,44 +27,44 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
errorExpected: true,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "replace path",
|
||||
config: dynamic.RequestRedirect{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantURL: "http://foo.com:80/baz",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace path without trailing slash",
|
||||
config: dynamic.RequestRedirect{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
expectedURL: "http://foo.com:80/baz",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
wantURL: "http://foo.com:80/baz",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace path with trailing slash",
|
||||
config: dynamic.RequestRedirect{
|
||||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantURL: "http://foo.com:80/baz/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "only hostname",
|
||||
config: dynamic.RequestRedirect{
|
||||
Hostname: ptr.To("bar.com"),
|
||||
},
|
||||
url: "http://foo.com:8080/foo/",
|
||||
expectedURL: "http://bar.com:8080/foo/",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:8080/foo/",
|
||||
wantURL: "http://bar.com:8080/foo/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path",
|
||||
|
@ -76,9 +72,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path with trailing slash",
|
||||
|
@ -86,9 +82,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
expectedURL: "http://foo.com:80/baz/bar/",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
wantURL: "http://foo.com:80/baz/bar/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
|
@ -96,9 +92,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
|
@ -106,9 +102,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo/"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "simple redirection",
|
||||
|
@ -117,9 +113,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Hostname: ptr.To("foobar.com"),
|
||||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo.com:80",
|
||||
expectedURL: "https://foobar.com:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo.com:80",
|
||||
wantURL: "https://foobar.com:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS permanent",
|
||||
|
@ -127,9 +123,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("https"),
|
||||
StatusCode: http.StatusMovedPermanently,
|
||||
},
|
||||
url: "http://foo",
|
||||
expectedURL: "https://foo",
|
||||
expectedStatus: http.StatusMovedPermanently,
|
||||
url: "http://foo",
|
||||
wantURL: "https://foo",
|
||||
wantStatus: http.StatusMovedPermanently,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS to HTTP permanent",
|
||||
|
@ -137,10 +133,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("http"),
|
||||
StatusCode: http.StatusMovedPermanently,
|
||||
},
|
||||
secured: true,
|
||||
url: "https://foo",
|
||||
expectedURL: "http://foo",
|
||||
expectedStatus: http.StatusMovedPermanently,
|
||||
url: "https://foo",
|
||||
wantURL: "http://foo",
|
||||
wantStatus: http.StatusMovedPermanently,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS",
|
||||
|
@ -148,9 +143,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("https"),
|
||||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
expectedURL: "https://foo:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo:80",
|
||||
wantURL: "https://foo:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS, with X-Forwarded-Proto",
|
||||
|
@ -158,12 +153,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("https"),
|
||||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
},
|
||||
expectedURL: "https://foo:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo:80",
|
||||
wantURL: "https://foo:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS to HTTP",
|
||||
|
@ -171,10 +163,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("http"),
|
||||
Port: ptr.To("80"),
|
||||
},
|
||||
secured: true,
|
||||
url: "https://foo:443",
|
||||
expectedURL: "http://foo:80",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "https://foo:443",
|
||||
wantURL: "http://foo:80",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTP",
|
||||
|
@ -182,9 +173,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("http"),
|
||||
Port: ptr.To("88"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
expectedURL: "http://foo:88",
|
||||
expectedStatus: http.StatusFound,
|
||||
url: "http://foo:80",
|
||||
wantURL: "http://foo:88",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -193,45 +184,32 @@ func TestRequestRedirectHandler(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 {
|
||||
handler, err := NewRequestRedirect(context.Background(), next, test.config, "traefikTest")
|
||||
if test.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, handler)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.wantStatus, recorder.Code)
|
||||
switch test.wantStatus {
|
||||
case http.StatusMovedPermanently, http.StatusFound:
|
||||
location, err := recorder.Result().Location()
|
||||
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:
|
||||
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)
|
||||
}
|
||||
assert.Equal(t, test.wantURL, location.String())
|
||||
default:
|
||||
location, err := recorder.Result().Location()
|
||||
require.Errorf(t, err, "Location %v", location)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
68
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go
Normal file
68
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package urlrewrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
typeName = "URLRewrite"
|
||||
)
|
||||
|
||||
type urlRewrite struct {
|
||||
name string
|
||||
next http.Handler
|
||||
|
||||
hostname *string
|
||||
path *string
|
||||
pathPrefix *string
|
||||
}
|
||||
|
||||
// NewURLRewrite creates a URL rewrite middleware.
|
||||
func NewURLRewrite(ctx context.Context, next http.Handler, conf dynamic.URLRewrite, name string) http.Handler {
|
||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
||||
logger.Debug().Msg("Creating middleware")
|
||||
|
||||
return urlRewrite{
|
||||
name: name,
|
||||
next: next,
|
||||
hostname: conf.Hostname,
|
||||
path: conf.Path,
|
||||
pathPrefix: conf.PathPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (u urlRewrite) GetTracingInformation() (string, string, trace.SpanKind) {
|
||||
return u.name, typeName, trace.SpanKindInternal
|
||||
}
|
||||
|
||||
func (u urlRewrite) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
newPath := req.URL.Path
|
||||
if u.path != nil && u.pathPrefix == nil {
|
||||
newPath = *u.path
|
||||
}
|
||||
if u.path != nil && u.pathPrefix != nil {
|
||||
newPath = path.Join(*u.path, strings.TrimPrefix(req.URL.Path, *u.pathPrefix))
|
||||
|
||||
// add the trailing slash if needed, as path.Join removes trailing slashes.
|
||||
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(newPath, "/") {
|
||||
newPath += "/"
|
||||
}
|
||||
}
|
||||
|
||||
req.URL.Path = newPath
|
||||
req.URL.RawPath = req.URL.EscapedPath()
|
||||
req.RequestURI = req.URL.RequestURI()
|
||||
|
||||
if u.hostname != nil {
|
||||
req.Host = *u.hostname
|
||||
}
|
||||
|
||||
u.next.ServeHTTP(rw, req)
|
||||
}
|
126
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go
Normal file
126
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package urlrewrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestURLRewriteHandler(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
config dynamic.URLRewrite
|
||||
url string
|
||||
wantURL string
|
||||
wantHost string
|
||||
}{
|
||||
{
|
||||
desc: "replace path",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace path without trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar/",
|
||||
wantURL: "http://foo.com/baz",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace path with trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "only host",
|
||||
config: dynamic.URLRewrite{
|
||||
Hostname: ptr.To("bar.com"),
|
||||
},
|
||||
url: "http://foo.com/foo/",
|
||||
wantURL: "http://foo.com/foo/",
|
||||
wantHost: "bar.com",
|
||||
},
|
||||
{
|
||||
desc: "host and path",
|
||||
config: dynamic.URLRewrite{
|
||||
Hostname: ptr.To("bar.com"),
|
||||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com/foo/",
|
||||
wantURL: "http://foo.com/baz/",
|
||||
wantHost: "bar.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path with trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar/",
|
||||
wantURL: "http://foo.com/baz/bar/",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo/"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
}
|
||||
|
||||
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 := NewURLRewrite(context.Background(), next, test.config, "traefikTest")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.wantURL, req.URL.String())
|
||||
assert.Equal(t, test.wantHost, req.Host)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue