From 2915fa0ffedb73c8cc979c144f7ce4b046006db8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 21:39:06 +0200 Subject: [PATCH] Zero allocation router, first commit --- context.go | 3 +- deprecated.go | 132 ------------ gin.go | 130 +++++++++--- path.go | 123 +++++++++++ path_test.go | 92 ++++++++ routergroup.go | 10 +- tree.go | 556 +++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 35 ++++ 8 files changed, 909 insertions(+), 172 deletions(-) create mode 100644 path.go create mode 100644 path_test.go create mode 100644 tree.go diff --git a/context.go b/context.go index c028a79c..b42c739d 100644 --- a/context.go +++ b/context.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) const AbortIndex = math.MaxInt8 / 2 @@ -26,7 +25,7 @@ type Context struct { Request *http.Request Writer ResponseWriter - Params httprouter.Params + Params Params Input inputHolder handlers []HandlerFunc index int8 diff --git a/deprecated.go b/deprecated.go index ebee67f5..b2e874f0 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,135 +3,3 @@ // license that can be found in the LICENSE file. package gin - -import ( - "log" - "net" - "net/http" - "strings" - - "github.com/gin-gonic/gin/binding" -) - -const ( - MIMEJSON = binding.MIMEJSON - MIMEHTML = binding.MIMEHTML - MIMEXML = binding.MIMEXML - MIMEXML2 = binding.MIMEXML2 - MIMEPlain = binding.MIMEPlain - MIMEPOSTForm = binding.MIMEPOSTForm - MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm -) - -// DEPRECATED, use Bind() instead. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) EnsureBody(item interface{}) bool { - return c.Bind(item) -} - -// DEPRECATED use bindings directly -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - return binding.JSON.Bind(c.Request, item) -} - -// DEPRECATED use gin.Static() instead -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (engine *Engine) ServeFiles(path string, root http.FileSystem) { - engine.router.ServeFiles(path, root) -} - -// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead -func (engine *Engine) LoadHTMLTemplates(pattern string) { - engine.LoadHTMLGlob(pattern) -} - -// DEPRECATED. Use NoRoute() instead -func (engine *Engine) NotFound404(handlers ...HandlerFunc) { - engine.NoRoute(handlers...) -} - -// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this -// middleware if you've got servers in front of this server. The list with (known) proxies and -// local ips are being filtered out of the forwarded for list, giving the last not local ip being -// the real client ip. -func ForwardedFor(proxies ...interface{}) HandlerFunc { - if len(proxies) == 0 { - // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} - - proxies = make([]interface{}, len(reservedLocalIps)) - - for i, v := range reservedLocalIps { - proxies[i] = v - } - } - - return func(c *Context) { - // the X-Forwarded-For header contains an array with left most the client ip, then - // comma separated, all proxies the request passed. The last proxy appears - // as the remote address of the request. Returning the client - // ip to comply with default RemoteAddr response. - - // check if remoteaddr is local ip or in list of defined proxies - remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) - - if !ipInMasks(remoteIp, proxies) { - return - } - - if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { - parts := strings.Split(forwardedFor, ",") - - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - - ip := net.ParseIP(strings.TrimSpace(part)) - - if ipInMasks(ip, proxies) { - continue - } - - // returning remote addr conform the original remote addr format - c.Request.RemoteAddr = ip.String() + ":0" - - // remove forwarded for address - c.Request.Header.Set("X-Forwarded-For", "") - return - } - } - } -} - -func ipInMasks(ip net.IP, masks []interface{}) bool { - for _, proxy := range masks { - var mask *net.IPNet - var err error - - switch t := proxy.(type) { - case string: - if _, mask, err = net.ParseCIDR(t); err != nil { - log.Panic(err) - } - case net.IP: - mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} - case net.IPNet: - mask = &t - } - - if mask.Contains(ip) { - return true - } - } - - return false -} diff --git a/gin.go b/gin.go index fa8b12dd..aec8467c 100644 --- a/gin.go +++ b/gin.go @@ -11,9 +11,32 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for i := range ps { + if ps[i].Key == name { + return ps[i].Value + } + } + return "" +} + +var default404Body = []byte("404 page not found") +var default405Body = []byte("405 method not allowed") type ( HandlerFunc func(*Context) @@ -21,32 +44,56 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - Default404Body []byte - Default405Body []byte - pool sync.Pool - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + pool sync.Pool + allNoRoute []HandlerFunc + allNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + trees map[string]*node + + // Enables automatic redirection if the current route can't be matched but a + // handler for the path with (without) the trailing slash exists. + // For example if /foo/ is requested but a route only exists for /foo, the + // client is redirected to /foo with http status code 301 for GET requests + // and 307 for all other request methods. + RedirectTrailingSlash bool + + // If enabled, the router tries to fix the current request path, if no + // handle is registered for it. + // First superfluous path elements like ../ or // are removed. + // Afterwards the router does a case-insensitive lookup of the cleaned path. + // If a handle can be found for this route, the router makes a redirection + // to the corrected path with status code 301 for GET requests and 307 for + // all other request methods. + // For example /FOO and /..//Foo could be redirected to /foo. + // RedirectTrailingSlash is independent of this option. + RedirectFixedPath bool + + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool } ) // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - engine := &Engine{} + engine := &Engine{ + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, + trees: make(map[string]*node), + } engine.RouterGroup = &RouterGroup{ Handlers: nil, absolutePath: "/", engine: engine, } - engine.router = httprouter.New() - engine.Default404Body = []byte("404 page not found") - engine.Default405Body = []byte("405 method not allowed") - engine.router.NotFound = engine.handle404 - engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { return engine.allocateContext() } @@ -67,13 +114,11 @@ func (engine *Engine) allocateContext() (context *Context) { return } -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context { c := engine.pool.Get().(*Context) c.reset() c.writermem.reset(w) c.Request = req - c.Params = params - c.handlers = handlers return c } @@ -132,39 +177,66 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) +func (engine *Engine) handle404(c *Context) { // set 404 by default, useful for logging + c.handlers = engine.allNoRoute c.Writer.WriteHeader(404) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 404 { - c.Data(-1, binding.MIMEPlain, engine.Default404Body) + c.Data(-1, binding.MIMEPlain, default404Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) } -func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoMethod) +func (engine *Engine) handle405(c *Context) { // set 405 by default, useful for logging + c.handlers = engine.allNoMethod c.Writer.WriteHeader(405) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 405 { - c.Data(-1, binding.MIMEPlain, engine.Default405Body) + c.Data(-1, binding.MIMEPlain, default405Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) +} + +func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { + if path[0] != '/' { + panic("path must begin with '/'") + } + + //methodCode := codeForHTTPMethod(method) + root := engine.trees[method] + if root == nil { + root = new(node) + engine.trees[method] = root + } + root.addRoute(path, handlers) } // ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - engine.router.ServeHTTP(writer, request) +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req) + //methodCode := codeForHTTPMethod(req.Method) + if root := engine.trees[req.Method]; root != nil { + path := req.URL.Path + if handlers, params, _ := root.getValue(path, c.Params); handlers != nil { + c.handlers = handlers + c.Params = params + c.Next() + engine.reuseContext(c) + return + } + } + + // Handle 404 + engine.handle404(c) + engine.reuseContext(c) } func (engine *Engine) Run(addr string) error { diff --git a/path.go b/path.go new file mode 100644 index 00000000..40b63bdb --- /dev/null +++ b/path.go @@ -0,0 +1,123 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for p, eliminating . and .. elements. +// +// The following rules are applied iteratively until no further processing can +// be done: +// 1. Replace multiple slashes with a single slash. +// 2. Eliminate each . path name element (the current directory). +// 3. Eliminate each inner .. path name element (the parent directory) +// along with the non-.. element that precedes it. +// 4. Eliminate .. elements that begin a rooted path: +// that is, replace "/.." by "/" at the beginning of a path. +// +// If the result of this process is an empty string, "/" is returned +func CleanPath(p string) string { + // Turn empty string into "/" + if p == "" { + return "/" + } + + n := len(p) + var buf []byte + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + buf = make([]byte, n+1) + buf[0] = '/' + } + + trailing := n > 2 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp). So in contrast to the path package this + // loop has no expensive function calls (except 1x make) + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r++ + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 2 + + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // real path element. + // add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + if buf == nil { + return p[:w] + } + return string(buf[:w]) +} + +// internal helper to lazily create a buffer if necessary +func bufApp(buf *[]byte, s string, w int, c byte) { + if *buf == nil { + if s[w] == c { + return + } + + *buf = make([]byte, len(s)) + copy(*buf, s[:w]) + } + (*buf)[w] = c +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 00000000..7563cfc5 --- /dev/null +++ b/path_test.go @@ -0,0 +1,92 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + if s := CleanPath(test.path); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) + } + if s := CleanPath(test.result); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) + } + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + if allocs > 0 { + t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) + } + } +} diff --git a/routergroup.go b/routergroup.go index c70bb34e..3d58512f 100644 --- a/routergroup.go +++ b/routergroup.go @@ -7,8 +7,6 @@ package gin import ( "net/http" "path" - - "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -48,13 +46,7 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []Han absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) debugRoute(httpMethod, absolutePath, handlers) - - group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - context := group.engine.createContext(w, req, params, handlers) - context.Next() - context.Writer.WriteHeaderNow() - group.engine.reuseContext(context) - }) + group.engine.handle(httpMethod, absolutePath, handlers) } // POST is a shortcut for router.Handle("POST", path, handle) diff --git a/tree.go b/tree.go new file mode 100644 index 00000000..195fa694 --- /dev/null +++ b/tree.go @@ -0,0 +1,556 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "strings" + "unicode" +) + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +func countParams(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { + continue + } + n++ + } + if n >= 255 { + return 255 + } + return uint8(n) +} + +type nodeType uint8 + +const ( + static nodeType = 0 + param nodeType = 1 + catchAll nodeType = 2 +) + +type node struct { + path string + wildChild bool + nType nodeType + maxParams uint8 + indices string + children []*node + handlers []HandlerFunc + priority uint32 +} + +// increments priority of the given child and reorders if necessary +func (n *node) incrementChildPrio(pos int) int { + n.children[pos].priority++ + prio := n.children[pos].priority + + // adjust position (move to front) + newPos := pos + for newPos > 0 && n.children[newPos-1].priority < prio { + // swap node positions + tmpN := n.children[newPos-1] + n.children[newPos-1] = n.children[newPos] + n.children[newPos] = tmpN + + newPos-- + } + + // build new index char string + if newPos != pos { + n.indices = n.indices[:newPos] + // unchanged prefix, might be empty + n.indices[pos:pos+1] + // the index char we move + n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' + } + + return newPos +} + +// addRoute adds a node with the given handle to the path. +// Not concurrency-safe! +func (n *node) addRoute(path string, handlers []HandlerFunc) { + n.priority++ + numParams := countParams(path) + + // non-empty tree + if len(n.path) > 0 || len(n.children) > 0 { + walk: + for { + // Update maxParams of the current node + if numParams > n.maxParams { + n.maxParams = numParams + } + + // Find the longest common prefix. + // This also implies that the common prefix contains no ':' or '*' + // since the existing key can't contain those chars. + i := 0 + max := min(len(path), len(n.path)) + for i < max && path[i] == n.path[i] { + i++ + } + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + indices: n.indices, + children: n.children, + handlers: n.handlers, + priority: n.priority - 1, + } + + // Update maxParams (max of all children) + for i := range child.children { + if child.children[i].maxParams > child.maxParams { + child.maxParams = child.children[i].maxParams + } + } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handlers = nil + n.wildChild = false + } + + // Make new node a child of this node + if i < len(path) { + path = path[i:] + + if n.wildChild { + n = n.children[0] + n.priority++ + + // Update maxParams of the child node + if numParams > n.maxParams { + n.maxParams = numParams + } + numParams-- + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] { + // check for longer wildcard, e.g. :name and :names + if len(n.path) >= len(path) || path[len(n.path)] == '/' { + continue walk + } + } + + panic("conflict with wildcard route") + } + + c := path[0] + + // slash after param + if n.nType == param && c == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + continue walk + } + + // Check if a child with the next path byte exists + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + + // Otherwise insert it + if c != ':' && c != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{c}) + child := &node{ + maxParams: numParams, + } + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child + } + n.insertChild(numParams, path, handlers) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handlers != nil { + panic("a Handle is already registered for this path") + } + n.handlers = handlers + } + return + } + } else { // Empty tree + n.insertChild(numParams, path, handlers) + } +} + +func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) { + var offset int // already handled bytes of the path + + // find prefix until first wildcard (beginning with ':'' or '*'') + for i, max := 0, len(path); numParams > 0; i++ { + c := path[i] + if c != ':' && c != '*' { + continue + } + + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route conflicts with existing children") + } + + // find wildcard end (either '/' or path end) + end := i + 1 + for end < max && path[end] != '/' { + switch path[end] { + // the wildcard name must not contain ':' and '*' + case ':', '*': + panic("only one wildcard per path segment is allowed") + default: + end++ + } + } + + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name") + } + + if c == ':' { // param + // split path at the beginning of the wildcard + if i > 0 { + n.path = path[offset:i] + offset = i + } + + child := &node{ + nType: param, + maxParams: numParams, + } + n.children = []*node{child} + n.wildChild = true + n = child + n.priority++ + numParams-- + + // if the path doesn't end with the wildcard, then there + // will be another non-wildcard subpath starting with '/' + if end < max { + n.path = path[offset:end] + offset = end + + child := &node{ + maxParams: numParams, + priority: 1, + } + n.children = []*node{child} + n = child + } + + } else { // catchAll + if end != max || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all") + } + + n.path = path[offset:i] + + // first node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + } + n.children = []*node{child} + n.indices = string(path[i]) + n = child + n.priority++ + + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handlers: handlers, + priority: 1, + } + n.children = []*node{child} + + return + } + } + + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handlers = handlers +} + +// Returns the handle registered with the given path (key). The values of +// wildcards are saved to a map. +// If no handle can be found, a TSR (trailing slash redirect) recommendation is +// made if a handle exists with an extra (without the) trailing slash for the +// given path. +func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { +walk: // Outer loop for walking the tree + for { + if len(path) > len(n.path) { + if path[:len(n.path)] == n.path { + path = path[len(n.path):] + // If this node does not have a wildcard (param or catchAll) + // child, we can just look up the next child node and continue + // to walk down the tree + if !n.wildChild { + c := path[0] + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + n = n.children[i] + continue walk + } + } + + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + tsr = (path == "/" && n.handlers != nil) + return + + } + + // handle wildcard child + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[1:] + p[i].Value = path[:end] + + // we need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + n = n.children[0] + continue walk + } + + // ... but we can't + tsr = (len(path) == end+1) + return + } + + if handlers = n.handlers; handlers != nil { + return + } else if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + tsr = (n.path == "/" && n.handlers != nil) + } + + return + + case catchAll: + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[2:] + p[i].Value = path + + handlers = n.handlers + return + + default: + panic("Invalid node type") + } + } + } else if path == n.path { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if handlers = n.handlers; handlers != nil { + return + } + + // No handle found. Check if a handle for this path + a + // trailing slash exists for trailing slash recommendation + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + tsr = (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) + return + } + } + + return + } + + // Nothing found. We can recommend to redirect to the same URL with an + // extra trailing slash if a leaf exists for that path + tsr = (path == "/") || + (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && + path == n.path[:len(n.path)-1] && n.handlers != nil) + return + } +} + +// Makes a case-insensitive lookup of the given path and tries to find a handler. +// It can optionally also fix trailing slashes. +// It returns the case-corrected path and a bool indicating whether the lookup +// was successful. +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { + ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory + + // Outer loop for walking the tree + for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { + path = path[len(n.path):] + ciPath = append(ciPath, n.path...) + + if len(path) > 0 { + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + r := unicode.ToLower(rune(path[0])) + for i, index := range n.indices { + // must use recursive approach since both index and + // ToLower(index) could exist. We must check both. + if r == unicode.ToLower(index) { + out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) + if found { + return append(ciPath, out...), true + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + found = (fixTrailingSlash && path == "/" && n.handlers != nil) + return + } + + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + k := 0 + for k < len(path) && path[k] != '/' { + k++ + } + + // add param value to case insensitive path + ciPath = append(ciPath, path[:k]...) + + // we need to go deeper! + if k < len(path) { + if len(n.children) > 0 { + path = path[k:] + n = n.children[0] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true + } + return + } + + if n.handlers != nil { + return ciPath, true + } else if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/'), true + } + } + return + + case catchAll: + return append(ciPath, path...), true + + default: + panic("Invalid node type") + } + } else { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if n.handlers != nil { + return ciPath, true + } + + // No handle found. + // Try to fix the path by adding a trailing slash + if fixTrailingSlash { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + if (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) { + return append(ciPath, '/'), true + } + return + } + } + } + return + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath, true + } + if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && + strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && + n.handlers != nil { + return append(ciPath, n.path...), true + } + } + return +} diff --git a/utils.go b/utils.go index fee39910..de9c302d 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,18 @@ import ( "strings" ) +const ( + methodGET = iota + methodPOST = iota + methodPUT = iota + methodAHEAD = iota + methodOPTIONS = iota + methodDELETE = iota + methodCONNECT = iota + methodTRACE = iota + methodUnknown = iota +) + type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -80,3 +92,26 @@ func lastChar(str string) uint8 { func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } + +func codeForHTTPMethod(method string) int { + switch method { + case "GET": + return methodGET + case "POST": + return methodPOST + case "PUT": + return methodPUT + case "AHEAD": + return methodAHEAD + case "OPTIONS": + return methodOPTIONS + case "DELETE": + return methodDELETE + case "TRACE": + return methodTRACE + case "CONNECT": + return methodCONNECT + default: + return methodUnknown + } +}