1
0
Fork 0

Dynamic Configuration Refactoring

This commit is contained in:
Ludovic Fernandez 2018-11-14 10:18:03 +01:00 committed by Traefiker Bot
parent d3ae88f108
commit a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions

View file

@ -2,110 +2,87 @@ package redirect
import (
"bytes"
"fmt"
"context"
"html/template"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"text/template"
"github.com/containous/traefik/configuration"
"github.com/containous/traefik/config"
"github.com/containous/traefik/middlewares"
"github.com/urfave/negroni"
"github.com/containous/traefik/tracing"
"github.com/opentracing/opentracing-go/ext"
"github.com/vulcand/oxy/utils"
)
const (
defaultRedirectRegex = `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$`
typeName = "Redirect"
)
// NewEntryPointHandler create a new redirection handler base on entry point
func NewEntryPointHandler(dstEntryPoint *configuration.EntryPoint, permanent bool) (negroni.Handler, error) {
exp := regexp.MustCompile(`(:\d+)`)
match := exp.FindStringSubmatch(dstEntryPoint.Address)
if len(match) == 0 {
return nil, fmt.Errorf("bad Address format %q", dstEntryPoint.Address)
}
protocol := "http"
if dstEntryPoint.TLS != nil {
protocol = "https"
}
replacement := protocol + "://${1}" + match[0] + "${2}"
return NewRegexHandler(defaultRedirectRegex, replacement, permanent)
type redirect struct {
next http.Handler
regex *regexp.Regexp
replacement string
permanent bool
errHandler utils.ErrorHandler
name string
}
// NewRegexHandler create a new redirection handler base on regex
func NewRegexHandler(exp string, replacement string, permanent bool) (negroni.Handler, error) {
re, err := regexp.Compile(exp)
// 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)
if err != nil {
return nil, err
}
return &handler{
regexp: re,
replacement: replacement,
permanent: permanent,
return &redirect{
regex: re,
replacement: config.Replacement,
permanent: config.Permanent,
errHandler: utils.DefaultHandler,
next: next,
name: name,
}, nil
}
type handler struct {
regexp *regexp.Regexp
replacement string
permanent bool
errHandler utils.ErrorHandler
func (r *redirect) GetTracingInformation() (string, ext.SpanKindEnum) {
return r.name, tracing.SpanKindNoneEnum
}
func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
oldURL := rawURL(req)
// only continue if the Regexp param matches the URL
if !h.regexp.MatchString(oldURL) {
next.ServeHTTP(rw, 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 := h.regexp.ReplaceAllString(oldURL, h.replacement)
newURL := r.regex.ReplaceAllString(oldURL, r.replacement)
// replace any variables that may be in there
rewrittenURL := &bytes.Buffer{}
if err := applyString(newURL, rewrittenURL, req); err != nil {
h.errHandler.ServeHTTP(rw, req, err)
r.errHandler.ServeHTTP(rw, req, err)
return
}
// parse the rewritten URL and replace request URL with it
parsedURL, err := url.Parse(rewrittenURL.String())
if err != nil {
h.errHandler.ServeHTTP(rw, req, err)
r.errHandler.ServeHTTP(rw, req, err)
return
}
if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk {
if len(stripPrefix) > 0 {
parsedURL.Path = stripPrefix
}
}
if addPrefix, addPrefixOk := req.Context().Value(middlewares.AddPrefixKey).(string); addPrefixOk {
if len(addPrefix) > 0 {
parsedURL.Path = strings.Replace(parsedURL.Path, addPrefix, "", 1)
}
}
if replacePath, replacePathOk := req.Context().Value(middlewares.ReplacePathKey).(string); replacePathOk {
if len(replacePath) > 0 {
parsedURL.Path = replacePath
}
}
if newURL != oldURL {
handler := &moveHandler{location: parsedURL, permanent: h.permanent}
handler := &moveHandler{location: parsedURL, permanent: r.permanent}
handler.ServeHTTP(rw, req)
return
}
@ -114,7 +91,7 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
// make sure the request URI corresponds the rewritten URL
req.RequestURI = req.URL.RequestURI()
next.ServeHTTP(rw, req)
r.next.ServeHTTP(rw, req)
}
type moveHandler struct {
@ -124,21 +101,25 @@ type moveHandler struct {
func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", m.location.String())
status := http.StatusFound
if m.permanent {
status = http.StatusMovedPermanently
}
rw.WriteHeader(status)
rw.Write([]byte(http.StatusText(status)))
_, err := rw.Write([]byte(http.StatusText(status)))
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func rawURL(request *http.Request) string {
func rawURL(req *http.Request) string {
scheme := "http"
if request.TLS != nil || isXForwardedHTTPS(request) {
if req.TLS != nil || isXForwardedHTTPS(req) {
scheme = "https"
}
return strings.Join([]string{scheme, "://", request.Host, request.RequestURI}, "")
return strings.Join([]string{scheme, "://", req.Host, req.RequestURI}, "")
}
func isXForwardedHTTPS(request *http.Request) bool {
@ -147,17 +128,13 @@ func isXForwardedHTTPS(request *http.Request) bool {
return len(xForwardedProto) > 0 && xForwardedProto == "https"
}
func applyString(in string, out io.Writer, request *http.Request) error {
func applyString(in string, out io.Writer, req *http.Request) error {
t, err := template.New("t").Parse(in)
if err != nil {
return err
}
data := struct {
Request *http.Request
}{
Request: request,
}
data := struct{ Request *http.Request }{Request: req}
return t.Execute(out, data)
}

View file

@ -1,146 +1,129 @@
package redirect
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/configuration"
"github.com/containous/traefik/config"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/tls"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewEntryPointHandler(t *testing.T) {
testCases := []struct {
desc string
entryPoint *configuration.EntryPoint
permanent bool
url string
expectedURL string
expectedStatus int
errorExpected bool
}{
{
desc: "HTTP to HTTPS",
entryPoint: &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}},
url: "http://foo:80",
expectedURL: "https://foo:443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTPS to HTTP",
entryPoint: &configuration.EntryPoint{Address: ":80"},
url: "https://foo:443",
expectedURL: "http://foo:80",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to HTTP",
entryPoint: &configuration.EntryPoint{Address: ":88"},
url: "http://foo:80",
expectedURL: "http://foo:88",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to HTTPS permanent",
entryPoint: &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}},
permanent: true,
url: "http://foo:80",
expectedURL: "https://foo:443",
expectedStatus: http.StatusMovedPermanently,
},
{
desc: "HTTPS to HTTP permanent",
entryPoint: &configuration.EntryPoint{Address: ":80"},
permanent: true,
url: "https://foo:443",
expectedURL: "http://foo:80",
expectedStatus: http.StatusMovedPermanently,
},
{
desc: "invalid address",
entryPoint: &configuration.EntryPoint{Address: ":foo", TLS: &tls.TLS{}},
url: "http://foo:80",
errorExpected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
handler, err := NewEntryPointHandler(test.entryPoint, test.permanent)
if test.errorExpected {
require.Error(t, err)
} else {
require.NoError(t, err)
recorder := httptest.NewRecorder()
r := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
handler.ServeHTTP(recorder, r, nil)
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
assert.Equal(t, test.expectedStatus, recorder.Code)
}
})
}
}
func TestNewRegexHandler(t *testing.T) {
testCases := []struct {
desc string
regex string
replacement string
permanent bool
config config.Redirect
url string
expectedURL string
expectedStatus int
errorExpected bool
secured bool
}{
{
desc: "simple redirection",
regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
replacement: "https://${1}bar$2:443$4",
desc: "simple redirection",
config: config.Redirect{
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: "use request header",
regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
replacement: `https://${1}{{ .Request.Header.Get "X-Foo" }}$2:443$4`,
desc: "use request header",
config: config.Redirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: `https://${1}{{ .Request.Header.Get "X-Foo" }}$2:443$4`,
},
url: "http://foo.com:80",
expectedURL: "https://foobar.com:443",
expectedStatus: http.StatusFound,
},
{
desc: "URL doesn't match regex",
regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
replacement: "https://${1}bar$2:443$4",
desc: "URL doesn't match regex",
config: config.Redirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
},
url: "http://bar.com:80",
expectedStatus: http.StatusOK,
},
{
desc: "invalid rewritten URL",
regex: `^(.*)$`,
replacement: "http://192.168.0.%31/",
desc: "invalid rewritten URL",
config: config.Redirect{
Regex: `^(.*)$`,
Replacement: "http://192.168.0.%31/",
},
url: "http://foo.com:80",
expectedStatus: http.StatusBadGateway,
},
{
desc: "invalid regex",
regex: `^(.*`,
replacement: "$1",
desc: "invalid regex",
config: config.Redirect{
Regex: `^(.*`,
Replacement: "$1",
},
url: "http://foo.com:80",
errorExpected: true,
},
{
desc: "HTTP to HTTPS permanent",
config: config.Redirect{
Regex: `^http://`,
Replacement: "https://$1",
Permanent: true,
},
url: "http://foo",
expectedURL: "https://foo",
expectedStatus: http.StatusMovedPermanently,
},
{
desc: "HTTPS to HTTP permanent",
config: config.Redirect{
Regex: `https://foo`,
Replacement: "http://foo",
Permanent: true,
},
secured: true,
url: "https://foo",
expectedURL: "http://foo",
expectedStatus: http.StatusMovedPermanently,
},
{
desc: "HTTP to HTTPS",
config: config.Redirect{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
},
url: "http://foo:80",
expectedURL: "https://foo:443",
expectedStatus: http.StatusFound,
},
{
desc: "HTTPS to HTTP",
config: config.Redirect{
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: config.Redirect{
Regex: `http://foo:80`,
Replacement: "http://foo:88",
},
url: "http://foo:80",
expectedURL: "http://foo:88",
expectedStatus: http.StatusFound,
},
}
for _, test := range testCases {
@ -148,20 +131,23 @@ func TestNewRegexHandler(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
handler, err := NewRegexHandler(test.regex, test.replacement, test.permanent)
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
handler, err := New(context.Background(), next, test.config, "traefikTest")
if test.errorExpected {
require.Nil(t, handler)
require.Error(t, err)
require.Nil(t, handler)
} else {
require.NotNil(t, handler)
require.NoError(t, err)
require.NotNil(t, handler)
recorder := httptest.NewRecorder()
r := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
if test.secured {
r.TLS = &tls.ConnectionState{}
}
r.Header.Set("X-Foo", "bar")
next := func(rw http.ResponseWriter, req *http.Request) {}
handler.ServeHTTP(recorder, r, next)
handler.ServeHTTP(recorder, r)
if test.expectedStatus == http.StatusMovedPermanently || test.expectedStatus == http.StatusFound {
assert.Equal(t, test.expectedStatus, recorder.Code)