1
0
Fork 0

Support HTTPRoute redirect port and scheme

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-06-13 11:16:04 +02:00 committed by GitHub
parent 27af1fb478
commit 3ca667a3d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1063 additions and 997 deletions

View file

@ -2,133 +2,101 @@ package redirect
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"path"
"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"
typeName = "RequestRedirect"
)
var uriRegexp = regexp.MustCompile(`^(https?):\/\/(\[[\w:.]+\]|[\w\._-]+)?(:\d+)?(.*)$`)
type redirect struct {
name string
next http.Handler
scheme *string
hostname *string
port *string
path *string
pathPrefix *string
statusCode int
}
// 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
statusCode := conf.StatusCode
if statusCode == 0 {
statusCode = http.StatusFound
}
if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound {
return nil, fmt.Errorf("unsupported status code: %d", statusCode)
}
return &redirect{
regex: re,
replacement: conf.Replacement,
permanent: conf.Permanent,
errHandler: utils.DefaultHandler,
next: next,
name: name,
rawURL: rawURL,
return redirect{
name: name,
next: next,
scheme: conf.Scheme,
hostname: conf.Hostname,
port: conf.Port,
path: conf.Path,
pathPrefix: conf.PathPrefix,
statusCode: statusCode,
}, 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) {
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)
func (r redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
redirectURL := *req.URL
redirectURL.Host = req.Host
// If the Regexp doesn't match, skip to the next handler.
if !r.regex.MatchString(oldURL) {
r.next.ServeHTTP(rw, req)
return
if r.scheme != nil {
redirectURL.Scheme = *r.scheme
}
// 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
host := redirectURL.Hostname()
if r.hostname != nil {
host = *r.hostname
}
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
port := redirectURL.Port()
if r.port != nil {
port = *r.port
}
if m.permanent {
status = http.StatusMovedPermanently
if req.Method != http.MethodGet {
status = http.StatusPermanentRedirect
if port != "" {
host = net.JoinHostPort(host, port)
}
redirectURL.Host = host
if r.path != nil && r.pathPrefix == nil {
redirectURL.Path = *r.path
}
if r.path != nil && r.pathPrefix != nil {
redirectURL.Path = path.Join(*r.path, strings.TrimPrefix(req.URL.Path, *r.pathPrefix))
// add the trailing slash if needed, as path.Join removes trailing slashes.
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(redirectURL.Path, "/") {
redirectURL.Path += "/"
}
}
rw.WriteHeader(status)
_, err := rw.Write([]byte(http.StatusText(status)))
if err != nil {
rw.Header().Set("Location", redirectURL.String())
rw.WriteHeader(r.statusCode)
if _, err := rw.Write([]byte(http.StatusText(r.statusCode))); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}

View file

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"k8s.io/utils/ptr"
)
func TestRequestRedirectHandler(t *testing.T) {
@ -24,49 +25,107 @@ func TestRequestRedirectHandler(t *testing.T) {
expectedStatus int
errorExpected bool
}{
{
desc: "wrong status code",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
StatusCode: http.StatusOK,
},
url: "http://foo.com:80/foo/bar",
errorExpected: 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,
},
{
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,
},
{
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,
},
{
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,
},
{
desc: "replace prefix path",
config: dynamic.RequestRedirect{
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,
},
{
desc: "replace prefix path with trailing slash",
config: dynamic.RequestRedirect{
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,
},
{
desc: "replace prefix path without slash prefix",
config: dynamic.RequestRedirect{
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,
},
{
desc: "replace prefix path without slash prefix",
config: dynamic.RequestRedirect{
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,
},
{
desc: "simple redirection",
config: dynamic.RequestRedirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
Scheme: ptr.To("https"),
Hostname: ptr.To("foobar.com"),
Port: ptr.To("443"),
},
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,
Scheme: ptr.To("https"),
StatusCode: http.StatusMovedPermanently,
},
url: "http://foo",
expectedURL: "https://foo",
@ -75,9 +134,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTPS to HTTP permanent",
config: dynamic.RequestRedirect{
Regex: `https://foo`,
Replacement: "http://foo",
Permanent: true,
Scheme: ptr.To("http"),
StatusCode: http.StatusMovedPermanently,
},
secured: true,
url: "https://foo",
@ -87,8 +145,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTPS",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
expectedURL: "https://foo:443",
@ -97,8 +155,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTPS, with X-Forwarded-Proto",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
headers: map[string]string{
@ -110,8 +168,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTPS to HTTP",
config: dynamic.RequestRedirect{
Regex: `https://foo:443`,
Replacement: "http://foo:80",
Scheme: ptr.To("http"),
Port: ptr.To("80"),
},
secured: true,
url: "https://foo:443",
@ -121,36 +179,13 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTP",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "http://foo:88",
Scheme: ptr.To("http"),
Port: ptr.To("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 {
@ -188,7 +223,7 @@ func TestRequestRedirectHandler(t *testing.T) {
assert.Equal(t, test.expectedStatus, recorder.Code)
switch test.expectedStatus {
case http.StatusMovedPermanently, http.StatusFound, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
case http.StatusMovedPermanently, http.StatusFound:
location, err := recorder.Result().Location()
require.NoError(t, err)