mirror of
https://github.com/gin-gonic/gin.git
synced 2025-12-11 19:47:00 +08:00
568 lines
24 KiB
Go
568 lines
24 KiB
Go
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be found
|
||
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
|
||
|
||
package gin
|
||
|
||
import (
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// Used as a workaround since we can't compare functions or their addresses
|
||
var fakeHandlerValue string
|
||
|
||
func fakeHandler(val string) HandlersChain {
|
||
return HandlersChain{func(c *Context) {
|
||
fakeHandlerValue = val
|
||
}}
|
||
}
|
||
|
||
type testRequests []struct {
|
||
path string
|
||
nilHandler bool
|
||
route string
|
||
ps Params
|
||
}
|
||
|
||
func getParams() *Params {
|
||
ps := make(Params, 0, 20)
|
||
return &ps
|
||
}
|
||
|
||
func getSkippedNodes() *[]skippedNode {
|
||
ps := make([]skippedNode, 0, 20)
|
||
return &ps
|
||
}
|
||
|
||
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
|
||
unescape := false
|
||
if len(unescapes) >= 1 {
|
||
unescape = unescapes[0]
|
||
}
|
||
|
||
for _, request := range requests {
|
||
value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
|
||
|
||
if value.handlers == nil {
|
||
if !request.nilHandler {
|
||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||
}
|
||
} else if request.nilHandler {
|
||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||
} else {
|
||
value.handlers[0](nil)
|
||
if fakeHandlerValue != request.route {
|
||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||
}
|
||
}
|
||
|
||
if value.params != nil {
|
||
if !reflect.DeepEqual(*value.params, request.ps) {
|
||
t.Errorf("Params mismatch for route '%s'", request.path)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
func checkPriorities(t *testing.T, n *node) uint32 {
|
||
var prio uint32
|
||
for i := range n.children {
|
||
prio += checkPriorities(t, n.children[i])
|
||
}
|
||
|
||
if n.handlers != nil {
|
||
prio++
|
||
}
|
||
|
||
if n.priority != prio {
|
||
t.Errorf(
|
||
"priority mismatch for node '%s': is %d, should be %d",
|
||
n.path, n.priority, prio,
|
||
)
|
||
}
|
||
|
||
return prio
|
||
}
|
||
|
||
func TestCountParams(t *testing.T) {
|
||
if countParams("/path/:param1/static/*catch-all") != 2 {
|
||
t.Fail()
|
||
}
|
||
if countParams(strings.Repeat("/:param", 256)) != 256 {
|
||
t.Fail()
|
||
}
|
||
// Test overflow protection - should cap at max uint16 (0xFFFF = 65535)
|
||
// Create a path with more than 65535 params (colons + stars)
|
||
// Need 65536+ colons to trigger the overflow check
|
||
overflowPath := strings.Repeat(":", 70000) // 70000 colons
|
||
if countParams(overflowPath) != 0xFFFF {
|
||
t.Errorf("countParams overflow protection failed: expected 0xFFFF, got %d", countParams(overflowPath))
|
||
}
|
||
}
|
||
|
||
func TestCountSections(t *testing.T) {
|
||
if countSections("/path/to/resource") != 3 {
|
||
t.Fail()
|
||
}
|
||
// Test overflow protection - should cap at max uint16 (0xFFFF = 65535)
|
||
// Create a path with more than 65535 slashes
|
||
// Need 65536+ slashes to trigger the overflow check
|
||
overflowPath := strings.Repeat("/", 70000) // 70000 slashes
|
||
if countSections(overflowPath) != 0xFFFF {
|
||
t.Errorf("countSections overflow protection failed: expected 0xFFFF, got %d", countSections(overflowPath))
|
||
}
|
||
}
|
||
|
||
func TestTreeAddAndGet(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/hi",
|
||
"/contact",
|
||
"/co",
|
||
"/c",
|
||
"/a",
|
||
"/ab",
|
||
"/doc/",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/α",
|
||
"/β",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
checkRequests(t, tree, testRequests{
|
||
{"/a", false, "/a", nil},
|
||
{"/", true, "", nil},
|
||
{"/hi", false, "/hi", nil},
|
||
{"/contact", false, "/contact", nil},
|
||
{"/co", false, "/co", nil},
|
||
{"/con", true, "", nil}, // key mismatch
|
||
{"/cona", true, "", nil}, // key mismatch
|
||
{"/no", true, "", nil}, // no matching child
|
||
{"/ab", false, "/ab", nil},
|
||
{"/α", false, "/α", nil},
|
||
{"/β", false, "/β", nil},
|
||
})
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
func TestTreeWildcard(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/",
|
||
"/cmd/:tool/",
|
||
"/cmd/:tool/:sub",
|
||
"/cmd/whoami",
|
||
"/cmd/whoami/root",
|
||
"/cmd/whoami/root/",
|
||
"/src/*filepath",
|
||
"/search/",
|
||
"/search/:query",
|
||
"/search/gin-gonic",
|
||
"/search/google",
|
||
"/user_:name",
|
||
"/user_:name/about",
|
||
"/files/:dir/*filepath",
|
||
"/doc/",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/info/:user/public",
|
||
"/info/:user/project/:project",
|
||
"/info/:user/project/golang",
|
||
"/aa/*xx",
|
||
"/ab/*xx",
|
||
"/:cc",
|
||
"/c1/:dd/e",
|
||
"/c1/:dd/e1",
|
||
"/:cc/cc",
|
||
"/:cc/:dd/ee",
|
||
"/:cc/:dd/:ee/ff",
|
||
"/:cc/:dd/:ee/:ff/gg",
|
||
"/:cc/:dd/:ee/:ff/:gg/hh",
|
||
"/get/test/abc/",
|
||
"/get/:param/abc/",
|
||
"/something/:paramname/thirdthing",
|
||
"/something/secondthing/test",
|
||
"/get/abc",
|
||
"/get/:param",
|
||
"/get/abc/123abc",
|
||
"/get/abc/:param",
|
||
"/get/abc/123abc/xxx8",
|
||
"/get/abc/123abc/:param",
|
||
"/get/abc/123abc/xxx8/1234",
|
||
"/get/abc/123abc/xxx8/:param",
|
||
"/get/abc/123abc/xxx8/1234/ffas",
|
||
"/get/abc/123abc/xxx8/1234/:param",
|
||
"/get/abc/123abc/xxx8/1234/kkdd/12c",
|
||
"/get/abc/123abc/xxx8/1234/kkdd/:param",
|
||
"/get/abc/:param/test",
|
||
"/get/abc/123abd/:param",
|
||
"/get/abc/123abddd/:param",
|
||
"/get/abc/123/:param",
|
||
"/get/abc/123abg/:param",
|
||
"/get/abc/123abf/:param",
|
||
"/get/abc/123abfff/:param",
|
||
"/get/abc/escaped_colon/test\\:param",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
checkRequests(t, tree, testRequests{
|
||
{"/", false, "/", nil},
|
||
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||
{"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
|
||
{"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
|
||
{"/cmd/whoami", false, "/cmd/whoami", nil},
|
||
{"/cmd/whoami/", true, "/cmd/whoami", nil},
|
||
{"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
|
||
{"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
|
||
{"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
|
||
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
|
||
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||
{"/search/", false, "/search/", nil},
|
||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
|
||
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
|
||
{"/search/google", false, "/search/google", nil},
|
||
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
|
||
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
|
||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
|
||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
|
||
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
|
||
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
|
||
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
|
||
// * Error with argument being intercepted
|
||
// new PR handle (/all /all/cc /a/cc)
|
||
// fix PR: https://github.com/gin-gonic/gin/pull/2796
|
||
{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
|
||
{"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
|
||
{"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
|
||
{"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
|
||
{"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
|
||
{"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
|
||
{"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
|
||
{"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
|
||
{"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
|
||
{"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
|
||
{"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
|
||
{"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
|
||
{"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
|
||
{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
|
||
{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
|
||
{"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
|
||
{"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
|
||
{"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
|
||
{"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
|
||
{"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
|
||
{"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
|
||
{"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
|
||
{"/get/test/abc/", false, "/get/test/abc/", nil},
|
||
{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
|
||
{"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
|
||
{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
|
||
{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
|
||
{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
|
||
{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
|
||
{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
|
||
{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
|
||
{"/something/secondthing/test", false, "/something/secondthing/test", nil},
|
||
{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
|
||
{"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
|
||
{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
|
||
{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
|
||
{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
|
||
{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
|
||
{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
|
||
{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
|
||
{"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
|
||
{"/get/abc", false, "/get/abc", nil},
|
||
{"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
|
||
{"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
|
||
{"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
|
||
{"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
|
||
{"/get/abc/123abc", false, "/get/abc/123abc", nil},
|
||
{"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
|
||
{"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
|
||
{"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
|
||
{"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
|
||
{"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
|
||
{"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
|
||
{"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
|
||
{"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
|
||
{"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
|
||
{"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
|
||
{"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
|
||
{"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
|
||
{"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
|
||
{"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
|
||
{"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
|
||
{"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
|
||
{"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
|
||
{"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
|
||
{"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
|
||
{"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
|
||
{"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
|
||
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
|
||
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
|
||
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
|
||
})
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
func TestUnescapeParameters(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/",
|
||
"/cmd/:tool/:sub",
|
||
"/cmd/:tool/",
|
||
"/src/*filepath",
|
||
"/search/:query",
|
||
"/files/:dir/*filepath",
|
||
"/info/:user/project/:project",
|
||
"/info/:user",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
unescape := true
|
||
checkRequests(t, tree, testRequests{
|
||
{"/", false, "/", nil},
|
||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
|
||
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
|
||
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
|
||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
|
||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
|
||
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
|
||
{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
|
||
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
|
||
}, unescape)
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
// TestSecureParameterHandling tests the fixes for path traversal vulnerabilities:
|
||
// PRISMA-2022-0393: Path traversal due to multiple encodings in path parameters
|
||
// PRISMA-2022-0394: Path traversal due to wildcard parameters incorrectly decoding URIs recursively
|
||
func TestSecureParameterHandling(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/info/:user",
|
||
"/files/*filepath",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
// Test cases for PRISMA-2022-0393 (path parameters)
|
||
// These test that double-encoded parameters are not double-decoded
|
||
unescape := true
|
||
checkRequests(t, tree, testRequests{
|
||
// Normal case - single encoding works as expected
|
||
{"/info/user%2Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user/profile"}}},
|
||
|
||
// Double encoding - should only decode once
|
||
{"/info/user%252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user%2Fprofile"}}},
|
||
|
||
// Triple encoding - should only decode once
|
||
{"/info/user%25252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user%252Fprofile"}}},
|
||
|
||
// Mixed encoding - should only decode once
|
||
{"/info/%2Fuser%252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "/user%2Fprofile"}}},
|
||
|
||
// No encoding - should pass through unchanged
|
||
{"/info/user", false, "/info/:user", Params{Param{Key: "user", Value: "user"}}},
|
||
}, unescape)
|
||
|
||
// Test cases for PRISMA-2022-0394 (wildcard parameters)
|
||
// These test that double-encoded wildcards are not double-decoded
|
||
checkRequests(t, tree, testRequests{
|
||
// Normal case - single encoding works as expected
|
||
{"/files/path%2Fto%2Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path/to/file.txt"}}},
|
||
|
||
// Double encoding - should only decode once
|
||
{"/files/path%252Fto%252Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path%2Fto%2Ffile.txt"}}},
|
||
|
||
// Triple encoding - should only decode once
|
||
{"/files/path%25252Fto%25252Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path%252Fto%252Ffile.txt"}}},
|
||
|
||
// Mixed encoding - should only decode once
|
||
{"/files/%2Fpath%252Fto%2Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "//path%2Fto/file.txt"}}},
|
||
|
||
// No encoding - should pass through unchanged
|
||
{"/files/normal/file.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/normal/file.txt"}}},
|
||
}, unescape)
|
||
}
|
||
|
||
func catchPanic(testFunc func()) (recv any) {
|
||
defer func() {
|
||
recv = recover()
|
||
}()
|
||
|
||
testFunc()
|
||
return
|
||
}
|
||
|
||
type testRoute struct {
|
||
path string
|
||
conflict bool
|
||
}
|
||
|
||
func testRoutes(t *testing.T, routes []testRoute) {
|
||
tree := &node{}
|
||
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route.path, nil)
|
||
})
|
||
|
||
if route.conflict {
|
||
if recv == nil {
|
||
t.Errorf("no panic for conflicting route '%s'", route.path)
|
||
}
|
||
} else if recv != nil {
|
||
t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeWildcardConflict(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/cmd/:tool/:sub", false},
|
||
{"/cmd/vet", false},
|
||
{"/foo/bar", false},
|
||
{"/foo/:name", false},
|
||
{"/foo/:names", true},
|
||
{"/cmd/*path", true},
|
||
{"/cmd/:badvar", true},
|
||
{"/cmd/:tool/names", false},
|
||
{"/cmd/:tool/:badsub/details", true},
|
||
{"/src/*filepath", false},
|
||
{"/src/:file", true},
|
||
{"/src/static.json", true},
|
||
{"/src/*filepathx", true},
|
||
{"/src/", true},
|
||
{"/src/foo/bar", true},
|
||
{"/src1/", false},
|
||
{"/src1/*filepath", true},
|
||
{"/src2*filepath", true},
|
||
{"/src2/*filepath", false},
|
||
{"/search/:query", false},
|
||
{"/search/valid", false},
|
||
{"/user_:name", false},
|
||
{"/user_x", false},
|
||
{"/user_:name", false},
|
||
{"/id:id", false},
|
||
{"/id/:id", false},
|
||
{"/static/*file", false},
|
||
{"/static/", true},
|
||
{"/escape/test\\:d1", false},
|
||
{"/escape/test\\:d2", false},
|
||
{"/escape/test:param", false},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestCatchAllAfterSlash(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/non-leading-*catchall", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestTreeChildConflict(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/cmd/vet", false},
|
||
{"/cmd/:tool", false},
|
||
{"/cmd/:tool/:sub", false},
|
||
{"/cmd/:tool/misc", false},
|
||
{"/cmd/:tool/:othersub", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestWildcardConflictWithStringsCut(t *testing.T) {
|
||
// Test the strings.Cut usage in wildcard conflict detection (line 258 in tree.go)
|
||
tree := &node{}
|
||
|
||
// Add a route with a wildcard parameter
|
||
tree.addRoute("/user/:name", fakeHandler("/user/:name"))
|
||
|
||
// Try to add a conflicting route that will trigger the strings.Cut path
|
||
// This should panic with a wildcard conflict
|
||
recv := catchPanic(func() {
|
||
tree.addRoute("/user/:id/profile", fakeHandler("/user/:id/profile"))
|
||
})
|
||
|
||
if recv == nil {
|
||
t.Error("Expected panic for wildcard conflict, but got none")
|
||
}
|
||
}
|
||
|
||
func TestCatchAllConflictWithStringsCut(t *testing.T) {
|
||
// Test the strings.Cut usage in catch-all conflict detection (line 382 in tree.go)
|
||
tree := &node{}
|
||
|
||
// Add a route with a path segment
|
||
tree.addRoute("/files/list", fakeHandler("/files/list"))
|
||
|
||
// Try to add a catch-all route that conflicts
|
||
// This should panic with a catch-all conflict
|
||
recv := catchPanic(func() {
|
||
tree.addRoute("/files/*filepath", fakeHandler("/files/*filepath"))
|
||
})
|
||
|
||
if recv == nil {
|
||
t.Error("Expected panic for catch-all conflict, but got none")
|
||
}
|
||
|
||
// Also test with an empty children case to cover line 382 when len(n.children) == 0
|
||
tree2 := &node{}
|
||
tree2.addRoute("/docs/", fakeHandler("/docs/"))
|
||
|
||
recv2 := catchPanic(func() {
|
||
tree2.addRoute("/docs/*page", fakeHandler("/docs/*page"))
|
||
})
|
||
|
||
if recv2 == nil {
|
||
t.Error("Expected panic for catch-all conflict with empty children, but got none")
|
||
}
|
||
}
|