@@ -43,17 +43,17 @@ func TestAddTrailingSlashWithConfig(t *testing.T) {
4343 {
4444 whenURL : "http://localhost:1323/%5Cexample.com" ,
4545 expectPath : `/\example.com` ,
46- expectLocation : []string {`/example .com/` },
46+ expectLocation : []string {`/%5Cexample .com/` },
4747 },
4848 {
4949 whenURL : `http://localhost:1323/\example.com` ,
5050 expectPath : `/\example.com` ,
51- expectLocation : []string {`/example .com/` },
51+ expectLocation : []string {`/%5Cexample .com/` },
5252 },
5353 {
5454 whenURL : `http://localhost:1323/\\%5C////%5C\\\example.com` ,
5555 expectPath : `/\\\////\\\\example.com` ,
56- expectLocation : []string {`/example .com/` },
56+ expectLocation : []string {`/%5C%5C%5C////%5C%5C%5C%5Cexample .com/` },
5757 },
5858 {
5959 whenURL : "http://localhost:1323//example.com" ,
@@ -63,7 +63,7 @@ func TestAddTrailingSlashWithConfig(t *testing.T) {
6363 {
6464 whenURL : "http://localhost:1323/%5C%5C" ,
6565 expectPath : `/\\` ,
66- expectLocation : []string {`/` },
66+ expectLocation : []string {`/%5C%5C/ ` },
6767 },
6868 // control characters that browsers strip from URLs must not hide a `//` prefix (GHSA-v753-g4cw-jm48)
6969 {
@@ -74,7 +74,7 @@ func TestAddTrailingSlashWithConfig(t *testing.T) {
7474 {
7575 whenURL : "http://localhost:1323/%09%5Cevil.example/x" ,
7676 expectPath : "/\t \\ evil.example/x" ,
77- expectLocation : []string {`/%09\evil .example/x/` },
77+ expectLocation : []string {`/%09%5Cevil .example/x/` },
7878 },
7979 {
8080 whenURL : "http://localhost:1323/%09//evil.example/x" ,
@@ -200,17 +200,17 @@ func TestRemoveTrailingSlashWithConfig(t *testing.T) {
200200 {
201201 whenURL : "http://localhost:1323/%5Cexample.com/" ,
202202 expectPath : `/\example.com/` ,
203- expectLocation : []string {`/example .com` },
203+ expectLocation : []string {`/%5Cexample .com` },
204204 },
205205 {
206206 whenURL : `http://localhost:1323/\example.com/` ,
207207 expectPath : `/\example.com/` ,
208- expectLocation : []string {`/example .com` },
208+ expectLocation : []string {`/%5Cexample .com` },
209209 },
210210 {
211211 whenURL : `http://localhost:1323/\\%5C////%5C\\\example.com/` ,
212212 expectPath : `/\\\////\\\\example.com/` ,
213- expectLocation : []string {`/example .com` },
213+ expectLocation : []string {`/%5C%5C%5C////%5C%5C%5C%5Cexample .com` },
214214 },
215215 {
216216 whenURL : "http://localhost:1323//example.com/" ,
@@ -220,7 +220,7 @@ func TestRemoveTrailingSlashWithConfig(t *testing.T) {
220220 {
221221 whenURL : "http://localhost:1323/%5C%5C/" ,
222222 expectPath : `/\\/` ,
223- expectLocation : []string {`/` },
223+ expectLocation : []string {`/%5C%5C ` },
224224 },
225225 // control characters that browsers strip from URLs must not hide a `//` prefix (GHSA-v753-g4cw-jm48)
226226 {
@@ -231,7 +231,7 @@ func TestRemoveTrailingSlashWithConfig(t *testing.T) {
231231 {
232232 whenURL : "http://localhost:1323/%09%5Cevil.example/x/" ,
233233 expectPath : "/\t \\ evil.example/x/" ,
234- expectLocation : []string {`/%09\evil .example/x` },
234+ expectLocation : []string {`/%09%5Cevil .example/x` },
235235 },
236236 {
237237 whenURL : "http://localhost:1323/%09//evil.example/x/" ,
@@ -344,3 +344,164 @@ func TestSanitizeURI(t *testing.T) {
344344 })
345345 }
346346}
347+
348+ func TestTrailingSlashRedirectEscapedPath (t * testing.T ) {
349+ for _ , tc := range []struct { path , add , remove string }{
350+ {"/files/a%2Fb" , "/files/a%2Fb/" , "/files/a%2Fb" },
351+ {"/files/a%3Fb?x=1" , "/files/a%3Fb/?x=1" , "/files/a%3Fb?x=1" },
352+ {"/files/a%20b" , "/files/a%20b/" , "/files/a%20b" },
353+ {"/files/a%23b" , "/files/a%23b/" , "/files/a%23b" },
354+ {"/%5Cexample.com" , "/%5Cexample.com/" , "/%5Cexample.com" },
355+ {"/%2f%2Fexample.com" , "/%2f%2Fexample.com/" , "/%2f%2Fexample.com" },
356+ } {
357+ for _ , remove := range []bool {false , true } {
358+ name := "add"
359+ target , want := tc .path , tc .add
360+ mw := AddTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
361+ if remove {
362+ name = "remove"
363+ target , want = tc .add , tc .remove
364+ // Keep the original escaping when constructing the remove request.
365+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
366+ target = req .URL .EscapedPath () + "/"
367+ if req .URL .RawQuery != "" {
368+ target += "?" + req .URL .RawQuery
369+ }
370+ mw = RemoveTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
371+ }
372+ t .Run (name + tc .path , func (t * testing.T ) {
373+ e := echo .New ()
374+ rec := httptest .NewRecorder ()
375+ req := httptest .NewRequest (http .MethodGet , target , nil )
376+ assert .NoError (t , mw (func (echo.Context ) error { return nil })(e .NewContext (req , rec )))
377+ assert .Equal (t , want , rec .Header ().Get (echo .HeaderLocation ))
378+ })
379+ }
380+ }
381+ }
382+
383+ func TestRemoveTrailingSlashRedirectAfterEncodedSuffix (t * testing.T ) {
384+ for _ , target := range []string {"/files/a%2Fb%2f" , "/files/a%2Fb%2F" } {
385+ t .Run (target , func (t * testing.T ) {
386+ e := echo .New ()
387+ rec := httptest .NewRecorder ()
388+ req := httptest .NewRequest (http .MethodGet , target + "/?x=1" , nil )
389+ mw := RemoveTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
390+ assert .NoError (t , mw (func (echo.Context ) error { return nil })(e .NewContext (req , rec )))
391+ assert .Equal (t , target + "?x=1" , rec .Header ().Get (echo .HeaderLocation ))
392+ })
393+ }
394+ }
395+
396+ func TestTrailingSlashRedirectPreservesRoute (t * testing.T ) {
397+ for _ , remove := range []bool {false , true } {
398+ name , target , route := "add" , "/files/a%2Fb" , "/files/:id/"
399+ mw := AddTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
400+ if remove {
401+ name , target , route = "remove" , "/files/a%2Fb/" , "/files/:id"
402+ mw = RemoveTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
403+ }
404+ t .Run (name , func (t * testing.T ) {
405+ e := echo .New ()
406+ e .Pre (mw )
407+ e .GET (route , func (c echo.Context ) error { return c .String (http .StatusOK , c .Param ("id" )) })
408+ first := httptest .NewRecorder ()
409+ e .ServeHTTP (first , httptest .NewRequest (http .MethodGet , target , nil ))
410+ if ! assert .Equal (t , http .StatusMovedPermanently , first .Code ) {
411+ return
412+ }
413+ second := httptest .NewRecorder ()
414+ e .ServeHTTP (second , httptest .NewRequest (http .MethodGet , first .Header ().Get (echo .HeaderLocation ), nil ))
415+ assert .Equal (t , http .StatusOK , second .Code )
416+ assert .Equal (t , "a%2Fb" , second .Body .String ())
417+ })
418+ }
419+ }
420+
421+ func TestTrailingSlashForwardEscapedPath (t * testing.T ) {
422+ for _ , tc := range []struct { segment , want string }{
423+ {"a%2Fb" , "a%2Fb" },
424+ {"a%20b" , "a b" },
425+ {"a%3Fb" , "a?b" },
426+ {"a%23b" , "a#b" },
427+ {"caf%C3%A9" , "café" },
428+ } {
429+ for _ , remove := range []bool {false , true } {
430+ name , target , route := "add" , "/files/" + tc .segment + "?q=1" , "/files/:id/"
431+ mw := AddTrailingSlash ()
432+ if remove {
433+ name , target , route = "remove" , "/files/" + tc .segment + "/?q=1" , "/files/:id"
434+ mw = RemoveTrailingSlash ()
435+ }
436+ t .Run (name + "/" + tc .segment , func (t * testing.T ) {
437+ req := httptest .NewRequest (http .MethodGet , target , nil )
438+ hadRawPath := req .URL .RawPath != ""
439+ e := echo .New ()
440+ e .Pre (mw )
441+ e .GET (route , func (c echo.Context ) error {
442+ assert .Equal (t , c .Request ().URL .EscapedPath ()+ "?q=1" , c .Request ().RequestURI )
443+ assert .Equal (t , hadRawPath , c .Request ().URL .RawPath != "" )
444+ return c .String (http .StatusOK , c .Param ("id" ))
445+ })
446+ rec := httptest .NewRecorder ()
447+ e .ServeHTTP (rec , req )
448+ assert .Equal (t , http .StatusOK , rec .Code )
449+ assert .Equal (t , tc .want , rec .Body .String ())
450+ })
451+ }
452+ }
453+ }
454+
455+ func TestTrailingSlashPreservesEncodedLeadingSeparators (t * testing.T ) {
456+ for _ , path := range []string {"/%2Fdocs" , "/%5Cdocs" , "/%2f%2Fdocs" } {
457+ t .Run (path , func (t * testing.T ) {
458+ for _ , remove := range []bool {false , true } {
459+ target , want := path , path + "/"
460+ mw := AddTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
461+ if remove {
462+ target , want = path + "/" , path
463+ mw = RemoveTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : http .StatusMovedPermanently })
464+ }
465+ e := echo .New ()
466+ rec := httptest .NewRecorder ()
467+ assert .NoError (t , mw (func (echo.Context ) error { return nil })(e .NewContext (httptest .NewRequest (http .MethodGet , target , nil ), rec )))
468+ assert .Equal (t , want , rec .Header ().Get (echo .HeaderLocation ))
469+ }
470+ })
471+ }
472+ }
473+
474+ func TestTrailingSlashEncodedFinalSegment (t * testing.T ) {
475+ for _ , tc := range []struct {
476+ name , target , route , location string
477+ remove bool
478+ }{
479+ {"add" , "/files/a%2F" , "/files/:id/" , "/files/a%2F/" , false },
480+ {"remove" , "/files/a%2F/" , "/files/:id" , "/files/a%2F" , true },
481+ {"already removed" , "/files/a%2F" , "/files/:id" , "" , true },
482+ } {
483+ for _ , code := range []int {0 , http .StatusMovedPermanently } {
484+ t .Run (tc .name + map [bool ]string {true : "/redirect" , false : "/forward" }[code != 0 ], func (t * testing.T ) {
485+ e := echo .New ()
486+ mw := AddTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : code })
487+ if tc .remove {
488+ mw = RemoveTrailingSlashWithConfig (TrailingSlashConfig {RedirectCode : code })
489+ }
490+ e .Pre (mw )
491+ e .GET (tc .route , func (c echo.Context ) error { return c .String (http .StatusOK , c .Param ("id" )) })
492+ rec := httptest .NewRecorder ()
493+ e .ServeHTTP (rec , httptest .NewRequest (http .MethodGet , tc .target , nil ))
494+ if code != 0 && tc .location != "" {
495+ if ! assert .Equal (t , code , rec .Code ) {
496+ return
497+ }
498+ assert .Equal (t , tc .location , rec .Header ().Get (echo .HeaderLocation ))
499+ rec = httptest .NewRecorder ()
500+ e .ServeHTTP (rec , httptest .NewRequest (http .MethodGet , tc .location , nil ))
501+ }
502+ assert .Equal (t , http .StatusOK , rec .Code )
503+ assert .Equal (t , "a%2F" , rec .Body .String ())
504+ })
505+ }
506+ }
507+ }
0 commit comments