diff --git a/docs/reference/filters.md b/docs/reference/filters.md index 584333ba68..bf87dcbb97 100644 --- a/docs/reference/filters.md +++ b/docs/reference/filters.md @@ -365,6 +365,32 @@ Parameters: The replacement may contain [template placeholders](#template-placeholders). If a template placeholder can't be resolved then empty value is used for it. +### normalizePath + +Normalize the URL path by removing empty path segments and trailing slashes. + +Parameters: + +- The URL path + +Example: + +``` +all: * -> normalizePath() -> "https://backend.example.org/api/v1; +``` + +Requests to + +``` +https://backend.example.org//api/v1/ +https://backend.example.org//api/v1 +https://backend.example.org/api//v1 +https://backend.example.org/api//v1/ +https://backend.example.org/api/v1/ +``` + +will all be normalized to `https://backend.example.org/api/v1`.
 + ## HTTP Redirect ### redirectTo diff --git a/filters/builtin/builtin.go b/filters/builtin/builtin.go index 1003756c60..b0dc73611f 100644 --- a/filters/builtin/builtin.go +++ b/filters/builtin/builtin.go @@ -16,6 +16,7 @@ import ( "github.com/zalando/skipper/filters/fadein" "github.com/zalando/skipper/filters/flowid" logfilter "github.com/zalando/skipper/filters/log" + "github.com/zalando/skipper/filters/normalizepath" "github.com/zalando/skipper/filters/rfc" "github.com/zalando/skipper/filters/scheduler" "github.com/zalando/skipper/filters/sed" @@ -233,6 +234,7 @@ func Filters() []filters.Spec { consistenthash.NewConsistentHashKey(), consistenthash.NewConsistentHashBalanceFactor(), tls.New(), + normalizepath.NewNormalizePath(), } } diff --git a/filters/filters.go b/filters/filters.go index 358e666b17..110a970076 100644 --- a/filters/filters.go +++ b/filters/filters.go @@ -370,4 +370,5 @@ const ( SetFastCgiFilenameName = "setFastCgiFilename" DisableRatelimitName = "disableRatelimit" UnknownRatelimitName = "unknownRatelimit" + NormalizePath = "normalizePath" ) diff --git a/filters/normalizepath/normalizepath.go b/filters/normalizepath/normalizepath.go new file mode 100644 index 0000000000..b6c5dd6416 --- /dev/null +++ b/filters/normalizepath/normalizepath.go @@ -0,0 +1,38 @@ +package normalizepath + +import ( + "strings" + + "github.com/zalando/skipper/filters" +) + +const ( + Name = filters.NormalizePath +) + +type normalizePath struct{} + +func NewNormalizePath() filters.Spec { return normalizePath{} } + +func (spec normalizePath) Name() string { return "normalizePath" } + +func (spec normalizePath) CreateFilter(config []interface{}) (filters.Filter, error) { + return normalizePath{}, nil +} + +func (f normalizePath) Request(ctx filters.FilterContext) { + req := ctx.Request() + + segments := strings.Split(req.URL.Path, "/") + var filteredSegments []string + for _, seg := range segments { + if seg != "" { + filteredSegments = append(filteredSegments, seg) + } + } + normalizedPath := "/" + strings.Join(filteredSegments, "/") + + req.URL.Path = normalizedPath +} + +func (f normalizePath) Response(ctx filters.FilterContext) {} diff --git a/filters/normalizepath/normalizepath_test.go b/filters/normalizepath/normalizepath_test.go new file mode 100644 index 0000000000..58dbee3221 --- /dev/null +++ b/filters/normalizepath/normalizepath_test.go @@ -0,0 +1,59 @@ +package normalizepath + +import ( + "net/http" + "net/url" + "testing" + + "github.com/zalando/skipper/filters/filtertest" +) + +// TestNormalizePath tests the NormalizePath function in accordance to the +// https://opensource.zalando.com/restful-api-guidelines/#136 +// specifically the notion of normalization of request paths: +// +// All services should normalize request paths before processing by removing +// duplicate and trailing slashes. Hence, the following requests should refer +// to the same resource: +// GET /orders/{order-id} +// GET /orders/{order-id}/ +// GET /orders//{order-id} +func TestNormalizePath(t *testing.T) { + urls := []string{ + "/orders/{order-id}", + "/orders/{order-id}/", + "/orders//{order-id}", + "/orders/{order-id}//", + "/orders/{order-id}///", + "/orders///{order-id}//", + } + + for _, u := range urls { + req := &http.Request{URL: &url.URL{Path: u}} + ctx := &filtertest.Context{ + FRequest: req, + } + f, err := NewNormalizePath().CreateFilter(nil) + if err != nil { + t.Fatal(err) + } + f.Request(ctx) + if req.URL.Path != "/orders/{order-id}" { + t.Errorf("failed to normalize the path: %s", req.URL.Path) + } + } + + // Ensure that root paths work as expected + req := &http.Request{URL: &url.URL{Path: "/"}} + ctx := &filtertest.Context{ + FRequest: req, + } + f, err := NewNormalizePath().CreateFilter(nil) + if err != nil { + t.Fatal(err) + } + f.Request(ctx) + if req.URL.Path != "/" { + t.Errorf("unexpected URL path change: %s, expected /", req.URL.Path) + } +}