Skip to content

Commit

Permalink
Merge pull request #1104 from ReneWerner87/master
Browse files Browse the repository at this point in the history
🐛 UnescapePath not working
  • Loading branch information
Fenny authored Jan 11, 2021
2 parents 926b2dc + 2da673b commit 93022ee
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 58 deletions.
7 changes: 4 additions & 3 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
)

// Version of current fiber package
const Version = "2.3.2"
const Version = "2.3.3"

// Handler defines a function to serve HTTP requests.
type Handler = func(*Ctx) error
Expand Down Expand Up @@ -138,8 +138,9 @@ type Config struct {
Immutable bool `json:"immutable"`

// When set to true, 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.
// before setting the path for the context, so that the routing,
// the returning of the current url from the context `ctx.Path()`
// and the paramters `ctx.Params(%key%)` with decoded characters will work
//
// Default: false
UnescapePath bool `json:"unescape_path"`
Expand Down
58 changes: 58 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,64 @@ func Test_App_Use_Params(t *testing.T) {
})
}

func Test_App_Use_UnescapedPath(t *testing.T) {
app := New(Config{UnescapePath: true, CaseSensitive: true})

app.Use("/cRéeR/:param", func(c *Ctx) error {
utils.AssertEqual(t, "/cRéeR/اختبار", c.Path())
return c.SendString(c.Params("param"))
})

app.Use("/abc", func(c *Ctx) error {
utils.AssertEqual(t, "/AbC", c.Path())
return nil
})

resp, err := app.Test(httptest.NewRequest(MethodGet, "/cR%C3%A9eR/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1", 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)")
// check the param result
utils.AssertEqual(t, "اختبار", getString(body))

// with lowercase letters
resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code")
}

func Test_App_Use_CaseSensitive(t *testing.T) {
app := New(Config{CaseSensitive: true})

app.Use("/abc", func(c *Ctx) error {
return c.SendString(c.Path())
})

// wrong letters in the requested route -> 404
resp, err := app.Test(httptest.NewRequest(MethodGet, "/AbC", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code")

// right letters in the requrested route -> 200
resp, err = app.Test(httptest.NewRequest(MethodGet, "/abc", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")

// check the detected path when the case insensitive recognition is activated
app.config.CaseSensitive = false
// check the case sensitive feature
resp, err = app.Test(httptest.NewRequest(MethodGet, "/AbC", 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)")
// check the detected path result
utils.AssertEqual(t, "/AbC", getString(body))
}

func Test_App_Add_Method_Test(t *testing.T) {
app := New()
defer func() {
Expand Down
67 changes: 37 additions & 30 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,22 @@ const maxParams = 30
// Ctx represents the Context which hold the HTTP request and response.
// It has methods for the request query string, parameters, body, HTTP headers and so on.
type Ctx struct {
app *App // Reference to *App
route *Route // Reference to *Route
indexRoute int // Index of the current route
indexHandler int // Index of the current handler
method string // HTTP method
methodINT int // HTTP method INT equivalent
baseURI string // HTTP base uri
path string // Prettified HTTP path -> string copy from pathBuffer
pathBuffer []byte // Prettified HTTP path buffer
treePath string // Path for the search in the tree
pathOriginal string // Original HTTP path
values [maxParams]string // Route parameter values
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
matched bool // Non use route matched
app *App // Reference to *App
route *Route // Reference to *Route
indexRoute int // Index of the current route
indexHandler int // Index of the current handler
method string // HTTP method
methodINT int // HTTP method INT equivalent
baseURI string // HTTP base uri
path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer
pathBuffer []byte // HTTP path buffer
detectionPath string // Route detection path -> string copy from detectionPathBuffer
detectionPathBuffer []byte // HTTP detectionPath buffer
treePath string // Path for the search in the tree
pathOriginal string // Original HTTP path
values [maxParams]string // Route parameter values
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
matched bool // Non use route matched
}

// Range data for c.Range
Expand Down Expand Up @@ -88,7 +90,6 @@ func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) *Ctx {
// Reset matched flag
c.matched = false
// Set paths
c.pathBuffer = append(c.pathBuffer[0:0], fctx.URI().PathOriginal()...)
c.pathOriginal = getString(fctx.URI().PathOriginal())
// Set method
c.method = getString(fctx.Request.Header.Method())
Expand All @@ -98,7 +99,7 @@ func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) *Ctx {
// reset base uri
c.baseURI = ""
// Prettify path
c.prettifyPath()
c.configDependentPaths()
return c
}

Expand Down Expand Up @@ -674,17 +675,14 @@ func (c *Ctx) Params(key string, defaultValue ...string) string {
func (c *Ctx) Path(override ...string) string {
if len(override) != 0 && c.path != override[0] {
// Set new path to context
c.pathBuffer = append(c.pathBuffer[0:0], override[0]...)
c.pathOriginal = override[0]
// c.path = override[0]
// c.pathOriginal = c.path

// Set new path to request context
c.fasthttp.Request.URI().SetPath(c.pathOriginal)
// Prettify path
c.prettifyPath()
c.configDependentPaths()
}
return c.pathOriginal
return c.path
}

// Protocol contains the request protocol string: http or https for TLS requests.
Expand Down Expand Up @@ -1105,24 +1103,33 @@ func (c *Ctx) XHR() bool {
return utils.EqualFoldBytes(utils.UnsafeBytes(c.Get(HeaderXRequestedWith)), []byte("xmlhttprequest"))
}

// prettifyPath ...
func (c *Ctx) prettifyPath() {
// If UnescapePath enabled, we decode the path
// configDependentPaths set paths for route recognition and prepared paths for the user,
// here the features for caseSensitive, decoded paths, strict paths are evaluated
func (c *Ctx) configDependentPaths() {
c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...)
// If UnescapePath enabled, we decode the path and save it for the framework user
if c.app.config.UnescapePath {
c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer)
}
c.path = getString(c.pathBuffer)

// another path is specified which is for routing recognition only
// use the path that was changed by the previous configuration flags
c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...)
// If CaseSensitive is disabled, we lowercase the original path
if !c.app.config.CaseSensitive {
c.pathBuffer = utils.ToLowerBytes(c.pathBuffer)
c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer)
}
// If StrictRouting is disabled, we strip all trailing slashes
if !c.app.config.StrictRouting && len(c.pathBuffer) > 1 && c.pathBuffer[len(c.pathBuffer)-1] == '/' {
c.pathBuffer = utils.TrimRightBytes(c.pathBuffer, '/')
if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' {
c.detectionPathBuffer = utils.TrimRightBytes(c.detectionPathBuffer, '/')
}
c.path = getString(c.pathBuffer)
c.detectionPath = getString(c.detectionPathBuffer)

// Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed,
// since the first three characters area select a list of routes
c.treePath = c.treePath[0:0]
if len(c.path) >= 3 {
c.treePath = c.path[:3]
if len(c.detectionPath) >= 3 {
c.treePath = c.detectionPath[:3]
}
}
14 changes: 11 additions & 3 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1038,15 +1038,23 @@ func Benchmark_Ctx_Params(b *testing.B) {
// go test -run Test_Ctx_Path
func Test_Ctx_Path(t *testing.T) {
t.Parallel()
app := New()
app := New(Config{UnescapePath: true})
app.Get("/test/:user", func(c *Ctx) error {
utils.AssertEqual(t, "/test/john", c.Path())
utils.AssertEqual(t, "/Test/John", c.Path())
// not strict && case insensitive
utils.AssertEqual(t, "/ABC/", c.Path("/ABC/"))
utils.AssertEqual(t, "/test/john/", c.Path("/test/john/"))
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil))

// test with special chars
app.Get("/specialChars/:name", func(c *Ctx) error {
utils.AssertEqual(t, "/specialChars/créer", c.Path())
// unescape is also working if you set the path afterwards
utils.AssertEqual(t, "/اختبار/", c.Path("/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1/"))
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/specialChars/cr%C3%A9er", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
}
Expand Down
2 changes: 1 addition & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func methodExist(ctx *Ctx) (exist bool) {
continue
}
// Check if it matches the request path
match := route.match(ctx.path, ctx.pathOriginal, &ctx.values)
match := route.match(ctx.detectionPath, ctx.path, &ctx.values)
// No match, next route
if match {
// We matched
Expand Down
16 changes: 8 additions & 8 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,37 +227,37 @@ func findNextCharsetPosition(search string, charset []byte) int {
}

// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
func (routeParser *routeParser) getMatch(s, original string, params *[maxParams]string, partialCheck bool) bool {
func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool {
var i, paramsIterator, partLen int
for _, segment := range routeParser.segs {
partLen = len(s)
partLen = len(detectionPath)
// check const segment
if !segment.IsParam {
i = segment.Length
// is optional part or the const part must match with the given string
// check if the end of the segment is a optional slash
if segment.HasOptionalSlash && partLen == i-1 && s == segment.Const[:i-1] {
if segment.HasOptionalSlash && partLen == i-1 && detectionPath == segment.Const[:i-1] {
i--
} else if !(i <= partLen && s[:i] == segment.Const) {
} else if !(i <= partLen && detectionPath[:i] == segment.Const) {
return false
}
} else {
// determine parameter length
i = findParamLen(s, segment)
i = findParamLen(detectionPath, segment)
if !segment.IsOptional && i == 0 {
return false
}
// take over the params positions
params[paramsIterator] = original[:i]
params[paramsIterator] = path[:i]
paramsIterator++
}

// reduce founded part from the string
if partLen > 0 {
s, original = s[i:], original[i:]
detectionPath, path = detectionPath[i:], path[i:]
}
}
if len(s) != 0 && !partialCheck {
if len(detectionPath) != 0 && !partialCheck {
return false
}

Expand Down
26 changes: 13 additions & 13 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ type Route struct {
Handlers []Handler `json:"-"` // Ctx handlers
}

func (r *Route) match(path, original string, params *[maxParams]string) (match bool) {
// root path check
if r.root && path == "/" {
func (r *Route) match(detectionPath, path string, params *[maxParams]string) (match bool) {
// root detectionPath check
if r.root && detectionPath == "/" {
return true
// '*' wildcard matches any path
// '*' wildcard matches any detectionPath
} else if r.star {
if len(original) > 1 {
params[0] = original[1:]
if len(path) > 1 {
params[0] = path[1:]
} else {
params[0] = ""
}
Expand All @@ -71,19 +71,19 @@ func (r *Route) match(path, original string, params *[maxParams]string) (match b
// Does this route have parameters
if len(r.Params) > 0 {
// Match params
if match := r.routeParser.getMatch(path, original, params, r.use); match {
// Get params from the original path
if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
// Get params from the path detectionPath
return match
}
}
// Is this route a Middleware?
if r.use {
// Single slash will match or path prefix
if r.root || strings.HasPrefix(path, r.path) {
// Single slash will match or detectionPath prefix
if r.root || strings.HasPrefix(detectionPath, r.path) {
return true
}
// Check for a simple path match
} else if len(r.path) == len(path) && r.path == path {
// Check for a simple detectionPath match
} else if len(r.path) == len(detectionPath) && r.path == detectionPath {
return true
}
// No match
Expand All @@ -107,7 +107,7 @@ func (app *App) next(c *Ctx) (match bool, err error) {
route := tree[c.indexRoute]

// Check if it matches the request path
match = route.match(c.path, c.pathOriginal, &c.values)
match = route.match(c.detectionPath, c.path, &c.values)

// No match, next route
if !match {
Expand Down

0 comments on commit 93022ee

Please sign in to comment.