Support HTTPRoute redirect port and scheme
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
parent
27af1fb478
commit
3ca667a3d4
31 changed files with 1063 additions and 997 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue