diff --git a/app.go b/app.go index 792ac9397d..c2815ef71d 100644 --- a/app.go +++ b/app.go @@ -86,6 +86,11 @@ type Settings struct { // Default: false Immutable bool + // Converts all encoded characters in the route back before setting the path for the context, + // so that the routing can also work with urlencoded special characters + // Default: false + UnescapePath bool + // Enable or disable ETag header generation, since both weak and strong etags are generated // using the same hashing method (CRC-32). Weak ETags are the default when enabled. // Default value false diff --git a/ctx.go b/ctx.go index c6fc63a3e8..e35ab69003 100644 --- a/ctx.go +++ b/ctx.go @@ -987,10 +987,16 @@ func (ctx *Ctx) XHR() bool { // prettifyPath ... func (ctx *Ctx) prettifyPath() { + // If UnescapePath enabled, we decode the path + if ctx.app.Settings.UnescapePath { + pathBytes := getBytes(ctx.path) + pathBytes = fasthttp.AppendUnquotedArg(pathBytes[:0], pathBytes) + ctx.path = getString(pathBytes) + } // If CaseSensitive is disabled, we lowercase the original path if !ctx.app.Settings.CaseSensitive { // We are making a copy here to keep access to the original path - ctx.path = utils.ToLower(ctx.pathOriginal) + ctx.path = utils.ToLower(ctx.path) } // If StrictRouting is disabled, we strip all trailing slashes if !ctx.app.Settings.StrictRouting && len(ctx.path) > 1 && ctx.path[len(ctx.path)-1] == '/' { diff --git a/router_test.go b/router_test.go index b677ecfd9e..c50bc11c95 100644 --- a/router_test.go +++ b/router_test.go @@ -146,6 +146,32 @@ func Test_Route_Match_Middleware(t *testing.T) { utils.AssertEqual(t, "bar/fasel", getString(body)) } +func Test_Route_Match_UnescapedPath(t *testing.T) { + app := New(&Settings{UnescapePath: true}) + + app.Use("/créer", func(ctx *Ctx) { + ctx.Send("test") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := ioutil.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "test", getString(body)) + // without special chars + resp, err = app.Test(httptest.NewRequest(MethodGet, "/créer", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // check deactivated behavior + app.Settings.UnescapePath = false + resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") +} + func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { app := New() @@ -405,6 +431,23 @@ func Benchmark_Router_Handler_CaseSensitive(b *testing.B) { } } +// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_Unescape -benchmem -count=4 +func Benchmark_Router_Handler_Unescape(b *testing.B) { + app := New() + app.Settings.UnescapePath = true + registerDummyRoutes(app) + app.Delete("/créer", func(c *Ctx) {}) + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod(MethodDelete) + c.URI().SetPath("/cr%C3%A9er") + + for n := 0; n < b.N; n++ { + app.handler(c) + } +} + // go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_StrictRouting -benchmem -count=4 func Benchmark_Router_Handler_StrictRouting(b *testing.B) { app := New() diff --git a/utils_test.go b/utils_test.go index 396bdb9e3e..87eac653f1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -116,3 +116,17 @@ func Benchmark_Utils_getGroupPath(b *testing.B) { // func Benchmark_Utils_parseTokenList(b *testing.B) { // // TODO // } + +func Benchmark_Utils_Unescape(b *testing.B) { + unescaped := "" + dst := make([]byte, 0) + + for n := 0; n < b.N; n++ { + source := "/cr%C3%A9er" + pathBytes := getBytes(source) + pathBytes = fasthttp.AppendUnquotedArg(dst[:0], pathBytes) + unescaped = getString(pathBytes) + } + + utils.AssertEqual(b, "/créer", unescaped) +}