Skip to content

Commit

Permalink
Add global route validation option (#50)
Browse files Browse the repository at this point in the history
* feat: add option for limiting route params number and key param length

* feat: improve license notice

* feat: improve parseRoute error message
  • Loading branch information
tigerwill90 authored Nov 26, 2024
1 parent d8e739e commit 72fb441
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 26 deletions.
27 changes: 22 additions & 5 deletions fox.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ type Router struct {
ipStrategy ClientIPStrategy
mws []middleware
mu sync.Mutex
maxParams uint16
maxParamKeyBytes uint16
handleMethodNotAllowed bool
handleOptions bool
redirectTrailingSlash bool
Expand All @@ -127,6 +129,8 @@ func New(opts ...GlobalOption) *Router {
r.noMethod = DefaultMethodNotAllowedHandler
r.autoOptions = DefaultOptionsHandler
r.ipStrategy = noClientIPStrategy{}
r.maxParams = math.MaxUint16
r.maxParamKeyBytes = math.MaxUint16

for _, opt := range opts {
opt.applyGlob(r)
Expand Down Expand Up @@ -311,7 +315,7 @@ func (fox *Router) Iter() Iter {
return Iter{
tree: rt,
root: rt.root,
maxDepth: rt.maxDepth,
maxDepth: rt.depth,
}
}

Expand Down Expand Up @@ -397,7 +401,7 @@ func (fox *Router) newTree() *iTree {

// newRoute create a new route, apply route options and apply middleware on the handler.
func (fox *Router) newRoute(pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, uint32, error) {
n, endHost, err := parseRoute(pattern)
n, endHost, err := fox.parseRoute(pattern)
if err != nil {
return nil, 0, err
}
Expand Down Expand Up @@ -595,7 +599,7 @@ const (
)

// parseRoute parse and validate the route in a single pass.
func parseRoute(url string) (uint32, int, error) {
func (fox *Router) parseRoute(url string) (uint32, int, error) {

endHost := strings.IndexByte(url, '/')
if endHost == -1 {
Expand All @@ -619,6 +623,7 @@ func parseRoute(url string) (uint32, int, error) {
previous := stateDefault
paramCnt := uint32(0)
countStatic := 0
startParam := 0
inParam := false
nonNumeric := false // true once we've seen a letter or hyphen
partlen := 0
Expand All @@ -635,6 +640,11 @@ func parseRoute(url string) (uint32, int, error) {
}
inParam = false

paramLen := len(url[startParam:i])
if paramLen > int(fox.maxParamKeyBytes) {
return 0, -1, fmt.Errorf("%w: parameter key too large: max=%d got=%d", ErrInvalidRoute, fox.maxParamKeyBytes, paramLen)
}

if i+1 < len(url) && url[i+1] != delim && url[i+1] != '/' {
return 0, -1, fmt.Errorf("%w: illegal character '%s' after '{param}'", ErrInvalidRoute, string(url[i+1]))
}
Expand Down Expand Up @@ -662,6 +672,11 @@ func parseRoute(url string) (uint32, int, error) {
}
inParam = false

paramLen := len(url[startParam:i])
if paramLen > int(fox.maxParamKeyBytes) {
return 0, -1, fmt.Errorf("%w: parameter key too large: max=%d got=%d", ErrInvalidRoute, fox.maxParamKeyBytes, paramLen)
}

if i+1 < len(url) && url[i+1] != '/' {
return 0, -1, fmt.Errorf("%w: illegal character '%s' after '*{param}'", ErrInvalidRoute, string(url[i+1]))
}
Expand Down Expand Up @@ -690,13 +705,15 @@ func parseRoute(url string) (uint32, int, error) {

if url[i] == '{' {
state = stateParam
startParam = i + 1
paramCnt++
} else if url[i] == '*' {
if i < endHost {
return 0, -1, fmt.Errorf("%w: catch-all wildcard not supported in hostname", ErrInvalidRoute)
}
state = stateCatchAll
i++
startParam = i + 1
paramCnt++
} else {
countStatic++
Expand Down Expand Up @@ -737,8 +754,8 @@ func parseRoute(url string) (uint32, int, error) {
}
}

if paramCnt > math.MaxUint16 {
return 0, -1, fmt.Errorf("%w: too many params (%d)", ErrInvalidRoute, paramCnt)
if paramCnt > uint32(fox.maxParams) {
return 0, -1, fmt.Errorf("%w: too many params: max=%d got=%d", ErrInvalidRoute, fox.maxParams, paramCnt)
}

i++
Expand Down
35 changes: 31 additions & 4 deletions fox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2891,8 +2891,6 @@ func TestInfixWildcardTsr(t *testing.T) {

tree := f.getRoot()

fmt.Println(tree.root[0])

c := newTestContext(f)
n, tsr := lookupByPath(tree, tree.root[0].children[0], tc.path, c, false)
require.NotNil(t, n)
Expand Down Expand Up @@ -3542,6 +3540,7 @@ func TestUpdateRoute(t *testing.T) {
}

func TestParseRoute(t *testing.T) {
f := New()
cases := []struct {
wantErr error
name string
Expand Down Expand Up @@ -3963,20 +3962,48 @@ func TestParseRoute(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
n, _, err := parseRoute(tc.path)
n, _, err := f.parseRoute(tc.path)
require.ErrorIs(t, err, tc.wantErr)
assert.Equal(t, tc.wantN, n)
})
}
}

func TestParseRouteParamsConstraint(t *testing.T) {
t.Run("param limit", func(t *testing.T) {
f := New(WithMaxParams(3))
_, _, err := f.parseRoute("/{1}/{2}/{3}")
assert.NoError(t, err)
_, _, err = f.parseRoute("/{1}/{2}/{3}/{4}")
assert.Error(t, err)
_, _, err = f.parseRoute("/ab{1}/{2}/cd/{3}/{4}/ef")
assert.Error(t, err)
})
t.Run("param key limit", func(t *testing.T) {
f := New(WithMaxParamKeyBytes(3))
_, _, err := f.parseRoute("/{abc}/{abc}/{abc}")
assert.NoError(t, err)
_, _, err = f.parseRoute("/{abcd}/{abc}/{abc}")
assert.Error(t, err)
_, _, err = f.parseRoute("/{abc}/{abcd}/{abc}")
assert.Error(t, err)
_, _, err = f.parseRoute("/{abc}/{abc}/{abcd}")
assert.Error(t, err)
_, _, err = f.parseRoute("/{abc}/*{abcd}/{abc}")
assert.Error(t, err)
_, _, err = f.parseRoute("/{abc}/{abc}/*{abcdef}")
assert.Error(t, err)
})
}

func TestParseRouteMalloc(t *testing.T) {
f := New()
var (
n uint32
err error
)
allocs := testing.AllocsPerRun(100, func() {
n, _, err = parseRoute("{ab}.{c}.de{f}.com/foo/bar/*{bar}/x*{args}/y/*{z}/{b}")
n, _, err = f.parseRoute("{ab}.{c}.de{f}.com/foo/bar/*{bar}/x*{args}/y/*{z}/{b}")
})
assert.Equal(t, float64(0), allocs)
assert.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.23.0

require (
github.com/google/gofuzz v1.2.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
golang.org/x/sys v0.27.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
5 changes: 3 additions & 2 deletions internal/simplelru/lru.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Copyright 2014 HashiCorp, Inc. All rights reserved.
// Use of this source code is governed by a MPL 2.0
// license that can be found in the LICENSE_list file.

package simplelru

Expand Down
5 changes: 3 additions & 2 deletions internal/simplelru/lru_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Copyright 2014 HashiCorp, Inc. All rights reserved.
// Use of this source code is governed by a MPL 2.0
// license that can be found in the LICENSE_list file.

package simplelru

Expand Down
15 changes: 15 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ func WithOptionsHandler(handler HandlerFunc) GlobalOption {
})
}

// WithMaxParams set the maximum number of parameters allowed in a route. The default max is math.MaxUint16.
func WithMaxParams(max uint16) GlobalOption {
return globOptionFunc(func(router *Router) {
router.maxParams = max
})
}

// WithMaxParamKeyBytes set the maximum number of bytes allowed per parameter key in a route. The default max is
// math.MaxUint16.
func WithMaxParamKeyBytes(max uint16) GlobalOption {
return globOptionFunc(func(router *Router) {
router.maxParamKeyBytes = max
})
}

// WithMiddleware attaches middleware to the router or to a specific route. The middlewares are executed
// in the order they are added. When applied globally, the middleware affects all handlers, including special handlers
// such as NotFound, MethodNotAllowed, AutoOption, and the internal redirect handler.
Expand Down
16 changes: 8 additions & 8 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ type iTree struct {
fox *Router
root roots
maxParams uint32
maxDepth uint32
depth uint32
}

func (t *iTree) txn(cache bool) *tXn {
return &tXn{
tree: t,
root: t.root,
maxParams: t.maxParams,
maxDepth: t.maxDepth,
depth: t.depth,
cache: cache,
}
}
Expand All @@ -47,7 +47,7 @@ type tXn struct {
writable *simplelru.LRU[*node, any]
root roots
maxParams uint32
maxDepth uint32
depth uint32
cache bool
}

Expand All @@ -56,7 +56,7 @@ func (t *tXn) commit() *iTree {
root: t.root,
fox: t.tree.fox,
maxParams: t.maxParams,
maxDepth: t.maxDepth,
depth: t.depth,
}
nt.ctx = sync.Pool{
New: func() any {
Expand All @@ -77,7 +77,7 @@ func (t *tXn) clone() *tXn {
tree: t.tree,
root: t.root,
maxParams: t.maxParams,
maxDepth: t.maxDepth,
depth: t.depth,
}
return tx
}
Expand Down Expand Up @@ -632,8 +632,8 @@ func (t *tXn) updateMaxParams(max uint32) {

// updateMaxDepth perform an update only if max is greater than the current
func (t *tXn) updateMaxDepth(max uint32) {
if max > t.maxDepth {
t.maxDepth = max
if max > t.depth {
t.depth = max
}
}

Expand Down Expand Up @@ -682,7 +682,7 @@ func isRemovable(method string) bool {
func (t *iTree) allocateContext() *cTx {
params := make(Params, 0, t.maxParams)
tsrParams := make(Params, 0, t.maxParams)
skipNds := make(skippedNodes, 0, t.maxDepth)
skipNds := make(skippedNodes, 0, t.depth)
return &cTx{
params: &params,
skipNds: &skipNds,
Expand Down
4 changes: 2 additions & 2 deletions txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (txn *Txn) Delete(method, pattern string) error {
return fmt.Errorf("%w: missing http method", ErrInvalidRoute)
}

_, _, err := parseRoute(pattern)
_, _, err := txn.fox.parseRoute(pattern)
if err != nil {
return err
}
Expand Down Expand Up @@ -222,7 +222,7 @@ func (txn *Txn) Iter() Iter {
return Iter{
tree: txn.rootTxn.tree,
root: rt,
maxDepth: txn.rootTxn.maxDepth,
maxDepth: txn.rootTxn.depth,
}
}

Expand Down

0 comments on commit 72fb441

Please sign in to comment.