SchemeRedirect Middleware

Co-authored-by: jbdoumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
Gérald Croës 2019-01-21 23:30:04 -08:00 committed by Traefiker Bot
parent 04958c6951
commit a433e469cc
11 changed files with 407 additions and 66 deletions

View file

@ -20,11 +20,6 @@ func Test_doOnJSON(t *testing.T) {
"Network": "",
"Address": ":80",
"TLS": null,
"Redirect": {
"EntryPoint": "https",
"Regex": "",
"Replacement": ""
},
"Auth": null,
"Compress": false
},
@ -36,7 +31,6 @@ func Test_doOnJSON(t *testing.T) {
"Certificates": null,
"ClientCAFiles": null
},
"Redirect": null,
"Auth": null,
"Compress": false
}
@ -109,11 +103,6 @@ func Test_doOnJSON(t *testing.T) {
"Network": "",
"Address": ":80",
"TLS": null,
"Redirect": {
"EntryPoint": "https",
"Regex": "",
"Replacement": ""
},
"Auth": null,
"Compress": false
},
@ -125,7 +114,6 @@ func Test_doOnJSON(t *testing.T) {
"Certificates": null,
"ClientCAFiles": null
},
"Redirect": null,
"Auth": null,
"Compress": false
}

View file

@ -17,7 +17,8 @@ type Middleware struct {
Headers *Headers `json:"headers,omitempty"`
Errors *ErrorPage `json:"errors,omitempty"`
RateLimit *RateLimit `json:"rateLimit,omitempty"`
Redirect *Redirect `json:"redirect,omitempty"`
RedirectRegex *RedirectRegex `json:"redirectregex,omitempty"`
RedirectScheme *RedirectScheme `json:"redirectscheme,omitempty"`
BasicAuth *BasicAuth `json:"basicAuth,omitempty"`
DigestAuth *DigestAuth `json:"digestAuth,omitempty"`
ForwardAuth *ForwardAuth `json:"forwardAuth,omitempty"`
@ -229,13 +230,20 @@ func (r *RateLimit) SetDefaults() {
r.ExtractorFunc = "request.host"
}
// Redirect holds the redirection configuration of an entry point to another, or to an URL.
type Redirect struct {
// RedirectRegex holds the redirection configuration.
type RedirectRegex struct {
Regex string `json:"regex,omitempty"`
Replacement string `json:"replacement,omitempty"`
Permanent bool `json:"permanent,omitempty"`
}
// RedirectScheme holds the scheme redirection configuration.
type RedirectScheme struct {
Scheme string `json:"scheme,omitempty"`
Port string `json:"port,omitempty"`
Permanent bool `json:"permanent,omitempty"`
}
// ReplacePath holds the ReplacePath configuration.
type ReplacePath struct {
Path string `json:"path,omitempty"`
@ -247,7 +255,7 @@ type ReplacePathRegex struct {
Replacement string `json:"replacement,omitempty"`
}
// Retry contains request retry config
// Retry holds the retry configuration.
type Retry struct {
Attempts int `description:"Number of attempts" export:"true"`
}

View file

@ -6,9 +6,6 @@ logLevel = "DEBUG"
[entryPoints.http]
address = ":8888"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":8443"
[entryPoints.https.tls]
@ -92,9 +89,9 @@ logLevel = "DEBUG"
path = "/api"
[Middlewares.api-slash-replace-path.ReplacePath]
path = "/api/"
[Middlewares.redirect-https.redirect]
regex = "^(?:https?://)?([\\w\\._-]+)(?::\\d+)?(.*)$"
replacement = "https://${1}:8443${2}"
[Middlewares.redirect-https.redirectScheme]
scheme = "https"
port = "8443"
[Services]
[Services.service1]

View file

@ -53,8 +53,8 @@ frontendRedirect:
- traefik.routers.rt-frontendRedirect.entryPoints=frontendRedirect
- traefik.routers.rt-frontendRedirect.rule=Path:/test
- traefik.routers.rt-frontendRedirect.middlewares=redirecthttp
- traefik.middlewares.redirecthttp.redirect.regex=^(?:https?://)?([\w\._-]+)(?::\d+)?(.*)$$
- traefik.middlewares.redirecthttp.redirect.replacement=http://$${1}:8000$${2}
- traefik.middlewares.redirecthttp.redirectScheme.scheme=http
- traefik.middlewares.redirecthttp.redirectScheme.port=8000
- traefik.services.service3.loadbalancer.server.port=80
rateLimit:
image: containous/whoami

View file

@ -10,17 +10,11 @@ import (
"regexp"
"strings"
"github.com/containous/traefik/config"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/tracing"
"github.com/opentracing/opentracing-go/ext"
"github.com/vulcand/oxy/utils"
)
const (
typeName = "Redirect"
)
type redirect struct {
next http.Handler
regex *regexp.Regexp
@ -30,21 +24,17 @@ type redirect struct {
name string
}
// New creates a redirect middleware.
func New(ctx context.Context, next http.Handler, config config.Redirect, name string) (http.Handler, error) {
logger := middlewares.GetLogger(ctx, name, typeName)
logger.Debug("Creating middleware")
logger.Debugf("Setting up redirect %s -> %s", config.Regex, config.Replacement)
re, err := regexp.Compile(config.Regex)
// New creates a Redirect middleware.
func newRedirect(ctx context.Context, next http.Handler, regex string, replacement string, permanent bool, name string) (http.Handler, error) {
re, err := regexp.Compile(regex)
if err != nil {
return nil, err
}
return &redirect{
regex: re,
replacement: config.Replacement,
permanent: config.Permanent,
replacement: replacement,
permanent: permanent,
errHandler: utils.DefaultHandler,
next: next,
name: name,
@ -122,11 +112,32 @@ func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
func rawURL(req *http.Request) string {
scheme := "http"
host := req.Host
port := ""
uri := req.RequestURI
schemeRegex := `^(https?):\/\/([\w\._-]+)(:\d+)?(.*)$`
re, _ := regexp.Compile(schemeRegex)
if re.Match([]byte(req.RequestURI)) {
match := re.FindStringSubmatch(req.RequestURI)
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 || isXForwardedHTTPS(req) {
scheme = "https"
}
return strings.Join([]string{scheme, "://", req.Host, req.RequestURI}, "")
return strings.Join([]string{scheme, "://", host, port, uri}, "")
}
func isXForwardedHTTPS(request *http.Request) bool {

View file

@ -0,0 +1,22 @@
package redirect
import (
"context"
"net/http"
"github.com/containous/traefik/config"
"github.com/containous/traefik/middlewares"
)
const (
typeRegexName = "RedirectRegex"
)
// NewRedirectRegex creates a redirect middleware.
func NewRedirectRegex(ctx context.Context, next http.Handler, conf config.RedirectRegex, name string) (http.Handler, error) {
logger := middlewares.GetLogger(ctx, name, typeRegexName)
logger.Debug("Creating middleware")
logger.Debugf("Setting up redirection from %s to %s", conf.Regex, conf.Replacement)
return newRedirect(ctx, next, conf.Regex, conf.Replacement, conf.Permanent, name)
}

View file

@ -13,10 +13,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestNewRegexHandler(t *testing.T) {
func TestRedirectRegexHandler(t *testing.T) {
testCases := []struct {
desc string
config config.Redirect
config config.RedirectRegex
method string
url string
secured bool
@ -26,7 +26,7 @@ func TestNewRegexHandler(t *testing.T) {
}{
{
desc: "simple redirection",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
},
@ -36,7 +36,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "use request header",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: `https://${1}{{ .Request.Header.Get "X-Foo" }}$2:443$4`,
},
@ -46,7 +46,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "URL doesn't match regex",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
},
@ -55,7 +55,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "invalid rewritten URL",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^(.*)$`,
Replacement: "http://192.168.0.%31/",
},
@ -64,7 +64,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "invalid regex",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^(.*`,
Replacement: "$1",
},
@ -73,7 +73,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTP to HTTPS permanent",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^http://`,
Replacement: "https://$1",
Permanent: true,
@ -84,7 +84,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTPS to HTTP permanent",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `https://foo`,
Replacement: "http://foo",
Permanent: true,
@ -96,7 +96,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTP to HTTPS",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
},
@ -106,7 +106,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTPS to HTTP",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `https://foo:443`,
Replacement: "http://foo:80",
},
@ -117,7 +117,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTP to HTTP",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `http://foo:80`,
Replacement: "http://foo:88",
},
@ -127,7 +127,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTP to HTTP POST",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^http://`,
Replacement: "https://$1",
},
@ -138,7 +138,7 @@ func TestNewRegexHandler(t *testing.T) {
},
{
desc: "HTTP to HTTP POST permanent",
config: config.Redirect{
config: config.RedirectRegex{
Regex: `^http://`,
Replacement: "https://$1",
Permanent: true,
@ -156,7 +156,7 @@ func TestNewRegexHandler(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
handler, err := New(context.Background(), next, test.config, "traefikTest")
handler, err := NewRedirectRegex(context.Background(), next, test.config, "traefikTest")
if test.errorExpected {
require.Error(t, err)

View file

@ -0,0 +1,34 @@
package redirect
import (
"context"
"net/http"
"github.com/containous/traefik/middlewares"
"github.com/pkg/errors"
"github.com/containous/traefik/config"
)
const (
typeSchemeName = "RedirectScheme"
schemeRedirectRegex = `^(https?:\/\/)?([\w\._-]+)(:\d+)?(.*)$`
)
// NewRedirectScheme creates a new RedirectScheme middleware.
func NewRedirectScheme(ctx context.Context, next http.Handler, conf config.RedirectScheme, name string) (http.Handler, error) {
logger := middlewares.GetLogger(ctx, name, typeSchemeName)
logger.Debug("Creating middleware")
logger.Debugf("Setting up redirection to %s %s", conf.Scheme, conf.Port)
if len(conf.Scheme) == 0 {
return nil, errors.New("you must provide a target scheme")
}
port := ""
if len(conf.Port) > 0 && !(conf.Scheme == "http" && conf.Port == "80" || conf.Scheme == "https" && conf.Port == "443") {
port = ":" + conf.Port
}
return newRedirect(ctx, next, schemeRedirectRegex, conf.Scheme+"://${2}"+port+"${4}", conf.Permanent, name)
}

View file

@ -0,0 +1,250 @@
package redirect
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"regexp"
"testing"
"github.com/containous/traefik/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRedirectSchemeHandler(t *testing.T) {
testCases := []struct {
desc string
config config.RedirectScheme
method string
url string
secured bool
expectedURL string
expectedStatus int
errorExpected bool
}{
{
desc: "Without scheme",
config: config.RedirectScheme{},
url: "http://foo",
errorExpected: true,
},
{
desc: "HTTP to HTTPS",
config: config.RedirectScheme{
Scheme: "https",
},
url: "http://foo",
expectedURL: "https://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP with port to HTTPS without port",
config: config.RedirectScheme{
Scheme: "https",
},
url: "http://foo:8080",
expectedURL: "https://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP without port to HTTPS with port",
config: config.RedirectScheme{
Scheme: "https",
Port: "8443",
},
url: "http://foo",
expectedURL: "https://foo:8443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP with port to HTTPS with port",
config: config.RedirectScheme{
Scheme: "https",
Port: "8443",
},
url: "http://foo:8000",
expectedURL: "https://foo:8443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTPS with port to HTTPS with port",
config: config.RedirectScheme{
Scheme: "https",
Port: "8443",
},
url: "https://foo:8000",
expectedURL: "https://foo:8443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTPS with port to HTTPS without port",
config: config.RedirectScheme{
Scheme: "https",
},
url: "https://foo:8000",
expectedURL: "https://foo",
expectedStatus: http.StatusFound,
},
{
desc: "redirection to HTTPS without port from an URL already in https",
config: config.RedirectScheme{
Scheme: "https",
},
url: "https://foo:8000/theother",
expectedURL: "https://foo/theother",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to HTTPS permanent",
config: config.RedirectScheme{
Scheme: "https",
Port: "8443",
Permanent: true,
},
url: "http://foo",
expectedURL: "https://foo:8443",
expectedStatus: http.StatusMovedPermanently,
},
{
desc: "to HTTP 80",
config: config.RedirectScheme{
Scheme: "http",
Port: "80",
},
url: "http://foo:80",
expectedURL: "http://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to wss",
config: config.RedirectScheme{
Scheme: "wss",
Port: "9443",
},
url: "http://foo",
expectedURL: "wss://foo:9443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to wss without port",
config: config.RedirectScheme{
Scheme: "wss",
},
url: "http://foo",
expectedURL: "wss://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP with port to wss without port",
config: config.RedirectScheme{
Scheme: "wss",
},
url: "http://foo:5678",
expectedURL: "wss://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to HTTPS without port",
config: config.RedirectScheme{
Scheme: "https",
},
url: "http://foo:443",
expectedURL: "https://foo",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP port redirection",
config: config.RedirectScheme{
Scheme: "http",
Port: "8181",
},
url: "http://foo:8080",
expectedURL: "http://foo:8181",
expectedStatus: http.StatusFound,
},
{
desc: "HTTPS with port 80 to HTTPS without port",
config: config.RedirectScheme{
Scheme: "https",
},
url: "https://foo:80",
expectedURL: "https://foo",
expectedStatus: http.StatusFound,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
handler, err := NewRedirectScheme(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
}
r := httptest.NewRequest(method, test.url, nil)
if test.secured {
r.TLS = &tls.ConnectionState{}
}
r.Header.Set("X-Foo", "bar")
handler.ServeHTTP(recorder, r)
assert.Equal(t, test.expectedStatus, recorder.Code)
if test.expectedStatus == http.StatusMovedPermanently ||
test.expectedStatus == http.StatusFound ||
test.expectedStatus == http.StatusTemporaryRedirect ||
test.expectedStatus == http.StatusPermanentRedirect {
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
} else {
location, err := recorder.Result().Location()
require.Errorf(t, err, "Location %v", location)
}
schemeRegex := `^(https?):\/\/([\w\._-]+)(:\d+)?(.*)$`
re, _ := regexp.Compile(schemeRegex)
if re.Match([]byte(test.url)) {
match := re.FindStringSubmatch(test.url)
r.RequestURI = match[4]
handler.ServeHTTP(recorder, r)
assert.Equal(t, test.expectedStatus, recorder.Code)
if test.expectedStatus == http.StatusMovedPermanently ||
test.expectedStatus == http.StatusFound ||
test.expectedStatus == http.StatusTemporaryRedirect ||
test.expectedStatus == http.StatusPermanentRedirect {
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
} else {
location, err := recorder.Result().Location()
require.Errorf(t, err, "Location %v", location)
}
}
}
})
}
}

View file

@ -90,9 +90,12 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.average": "42",
"traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.burst": "42",
"traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.period": "42",
"traefik.middlewares.Middleware13.redirect.permanent": "true",
"traefik.middlewares.Middleware13.redirect.regex": "foobar",
"traefik.middlewares.Middleware13.redirect.replacement": "foobar",
"traefik.middlewares.Middleware13.redirectregex.permanent": "true",
"traefik.middlewares.Middleware13.redirectregex.regex": "foobar",
"traefik.middlewares.Middleware13.redirectregex.replacement": "foobar",
"traefik.middlewares.Middleware13b.redirectscheme.scheme": "https",
"traefik.middlewares.Middleware13b.redirectscheme.port": "80",
"traefik.middlewares.Middleware13b.redirectscheme.permanent": "true",
"traefik.middlewares.Middleware14.replacepath.path": "foobar",
"traefik.middlewares.Middleware15.replacepathregex.regex": "foobar",
"traefik.middlewares.Middleware15.replacepathregex.replacement": "foobar",
@ -237,12 +240,19 @@ func TestDecodeConfiguration(t *testing.T) {
},
},
"Middleware13": {
Redirect: &config.Redirect{
RedirectRegex: &config.RedirectRegex{
Regex: "foobar",
Replacement: "foobar",
Permanent: true,
},
},
"Middleware13b": {
RedirectScheme: &config.RedirectScheme{
Scheme: "https",
Port: "80",
Permanent: true,
},
},
"Middleware14": {
ReplacePath: &config.ReplacePath{
Path: "foobar",
@ -553,12 +563,19 @@ func TestEncodeConfiguration(t *testing.T) {
},
},
"Middleware13": {
Redirect: &config.Redirect{
RedirectRegex: &config.RedirectRegex{
Regex: "foobar",
Replacement: "foobar",
Permanent: true,
},
},
"Middleware13b": {
RedirectScheme: &config.RedirectScheme{
Scheme: "https",
Port: "80",
Permanent: true,
},
},
"Middleware14": {
ReplacePath: &config.ReplacePath{
Path: "foobar",
@ -856,9 +873,12 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Average": "42",
"traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Burst": "42",
"traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Period": "42",
"traefik.Middlewares.Middleware13.Redirect.Permanent": "true",
"traefik.Middlewares.Middleware13.Redirect.Regex": "foobar",
"traefik.Middlewares.Middleware13.Redirect.Replacement": "foobar",
"traefik.Middlewares.Middleware13.RedirectRegex.Regex": "foobar",
"traefik.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar",
"traefik.Middlewares.Middleware13.RedirectRegex.Permanent": "true",
"traefik.Middlewares.Middleware13b.RedirectScheme.Scheme": "https",
"traefik.Middlewares.Middleware13b.RedirectScheme.Port": "80",
"traefik.Middlewares.Middleware13b.RedirectScheme.Permanent": "true",
"traefik.Middlewares.Middleware14.ReplacePath.Path": "foobar",
"traefik.Middlewares.Middleware15.ReplacePathRegex.Regex": "foobar",
"traefik.Middlewares.Middleware15.ReplacePathRegex.Replacement": "foobar",

View file

@ -248,11 +248,22 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string, c
}
}
// Redirect
if config.Redirect != nil {
// RedirectRegex
if config.RedirectRegex != nil {
if middleware == nil {
middleware = func(next http.Handler) (http.Handler, error) {
return redirect.New(ctx, next, *config.Redirect, middlewareName)
return redirect.NewRedirectRegex(ctx, next, *config.RedirectRegex, middlewareName)
}
} else {
return nil, badConf
}
}
// RedirectScheme
if config.RedirectScheme != nil {
if middleware == nil {
middleware = func(next http.Handler) (http.Handler, error) {
return redirect.NewRedirectScheme(ctx, next, *config.RedirectScheme, middlewareName)
}
} else {
return nil, badConf