package router import ( "context" "net/http" "strings" "sync" ) // Handle is a function that can be registered to a route to handle HTTP // requests. Like http.HandlerFunc, but has a third parameter for the values of // wildcards (path variables). type Handle func(http.ResponseWriter, *http.Request, Params) // 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 _, p := range ps { if p.Key == name { return p.Value } } return "" } type paramsKey struct{} // ParamsKey is the request context key under which URL params are stored. var ParamsKey = paramsKey{} // ParamsFromContext pulls the URL parameters from a request context, // or returns nil if none are present. func ParamsFromContext(ctx context.Context) Params { p, _ := ctx.Value(ParamsKey).(Params) return p } // MatchedRoutePathParam is the Param name under which the path of the matched // route is stored, if Router.SaveMatchedRoutePath is set. var MatchedRoutePathParam = "$matchedRoutePath" // MatchedRoutePath retrieves the path of the matched route. // Router.SaveMatchedRoutePath must have been enabled when the respective // handler was added, otherwise this function always returns an empty string. func (ps Params) MatchedRoutePath() string { return ps.ByName(MatchedRoutePathParam) } // Router is a http.Handler which can be used to dispatch requests to different // handler functions via configurable routes type Router struct { trees map[string]*node paramsPool sync.Pool maxParams uint16 // If enabled, adds the matched route path onto the http.Request context // before invoking the handler. // The matched route path is only added to handlers of routes that were // registered when this option was enabled. SaveMatchedRoutePath bool // 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 308 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 308 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 // If enabled, the router automatically replies to OPTIONS requests. // Custom OPTIONS handlers take priority over automatic replies. HandleOPTIONS bool // An optional http.Handler that is called on automatic OPTIONS requests. // The handler is only called if HandleOPTIONS is true and no OPTIONS // handler for the specific path was set. // The "Allowed" header is set before calling the handler. GlobalOPTIONS http.Handler // Cached value of global (*) allowed methods globalAllowed string // Configurable http.Handler which is called when no matching route is // found. If it is not set, http.NotFound is used. NotFound http.Handler // Configurable http.Handler which is called when a request // cannot be routed and HandleMethodNotAllowed is true. // If it is not set, http.Error with http.StatusMethodNotAllowed is used. // The "Allow" header with allowed request methods is set before the handler // is called. MethodNotAllowed http.Handler // Function to handle panics recovered from http handlers. // It should be used to generate a error page and return the http error code // 500 (Internal Server Error). // The handler can be used to keep your server from crashing because of // unrecovered panics. PanicHandler func(http.ResponseWriter, *http.Request, interface{}) } // Make sure the Router conforms with the http.Handler interface var _ http.Handler = New() // New returns a new initialized Router. // Path auto-correction, including trailing slashes, is enabled by default. func New() *Router { return &Router{ RedirectTrailingSlash: true, RedirectFixedPath: true, HandleMethodNotAllowed: true, HandleOPTIONS: true, } } func (r *Router) getParams() *Params { ps, _ := r.paramsPool.Get().(*Params) *ps = (*ps)[0:0] // reset slice return ps } func (r *Router) putParams(ps *Params) { if ps != nil { r.paramsPool.Put(ps) } } func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { return func(w http.ResponseWriter, req *http.Request, ps Params) { if ps == nil { psp := r.getParams() ps = (*psp)[0:1] ps[0] = Param{Key: MatchedRoutePathParam, Value: path} handle(w, req, ps) r.putParams(psp) } else { ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) handle(w, req, ps) } } } // GET is a shortcut for router.Handle(http.MethodGet, path, handle) func (r *Router) GET(path string, handle Handle) { r.Handle(http.MethodGet, path, handle) } // HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) func (r *Router) HEAD(path string, handle Handle) { r.Handle(http.MethodHead, path, handle) } // OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) func (r *Router) OPTIONS(path string, handle Handle) { r.Handle(http.MethodOptions, path, handle) } // POST is a shortcut for router.Handle(http.MethodPost, path, handle) func (r *Router) POST(path string, handle Handle) { r.Handle(http.MethodPost, path, handle) } // PUT is a shortcut for router.Handle(http.MethodPut, path, handle) func (r *Router) PUT(path string, handle Handle) { r.Handle(http.MethodPut, path, handle) } // PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) func (r *Router) PATCH(path string, handle Handle) { r.Handle(http.MethodPatch, path, handle) } // DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) func (r *Router) DELETE(path string, handle Handle) { r.Handle(http.MethodDelete, path, handle) } // Replace registers or replace request handle with the given path and method. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut // functions can be used. // // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). func (r *Router) Replace(method, path string, handle Handle) { r.addRoute(method, path, true, handle) } // Handle registers a new request handle with the given path and method. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut // functions can be used. // // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). func (r *Router) Handle(method, path string, handle Handle) { r.addRoute(method, path, false, handle) } // addRoute registers a new request handle with the given path and method. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut // functions can be used. // // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). func (r *Router) addRoute(method, path string, replace bool, handle Handle) { varsCount := uint16(0) if method == "" { panic("method must not be empty") } if len(path) < 1 || path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } if handle == nil { panic("handle must not be nil") } if r.SaveMatchedRoutePath { varsCount++ handle = r.saveMatchedRoutePath(path, handle) } if r.trees == nil { r.trees = make(map[string]*node) } root := r.trees[method] if root == nil { root = new(node) r.trees[method] = root r.globalAllowed = r.allowed("*", "") } root.addRoute(path, handle, replace) // Update maxParams if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { r.maxParams = paramsCount + varsCount } // Lazy-init paramsPool alloc func if r.paramsPool.New == nil && r.maxParams > 0 { r.paramsPool.New = func() interface{} { ps := make(Params, 0, r.maxParams) return &ps } } } // Handler is an adapter which allows the usage of an http.Handler as a // request handle. // The Params are available in the request context under ParamsKey. func (r *Router) Handler(method, path string, handler http.Handler) { r.Handle(method, path, func(w http.ResponseWriter, req *http.Request, p Params) { if len(p) > 0 { ctx := req.Context() ctx = context.WithValue(ctx, ParamsKey, p) req = req.WithContext(ctx) } handler.ServeHTTP(w, req) }, ) } // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a // request handle. func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { r.Handler(method, path, handler) } // 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 (r *Router) ServeFiles(path string, root http.FileSystem) { if len(path) < 10 || path[len(path)-10:] != "/*filepath" { panic("path must end with /*filepath in path '" + path + "'") } fileServer := http.FileServer(root) r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { req.URL.Path = ps.ByName("filepath") fileServer.ServeHTTP(w, req) }) } func (r *Router) recv(w http.ResponseWriter, req *http.Request) { if rcv := recover(); rcv != nil { r.PanicHandler(w, req, rcv) } } // Lookup allows the manual lookup of a method + path combo. // This is e.g. useful to build a framework around this router. // If the path was found, it returns the handle function and the path parameter // values. Otherwise the third return value indicates whether a redirection to // the same path with an extra / without the trailing slash should be performed. func (r *Router) Lookup(method, path string) (Handle, Params, bool) { if root := r.trees[method]; root != nil { handle, ps, tsr := root.getValue(path, r.getParams) if handle == nil { r.putParams(ps) return nil, nil, tsr } if ps == nil { return handle, nil, tsr } return handle, *ps, tsr } return nil, nil, false } func (r *Router) allowed(path, reqMethod string) (allow string) { allowed := make([]string, 0, 9) if path == "*" { // server-wide // empty method is used for internal calls to refresh the cache if reqMethod == "" { for method := range r.trees { if method == http.MethodOptions { continue } // Add request method to list of allowed methods allowed = append(allowed, method) } } else { return r.globalAllowed } } else { // specific path for method := range r.trees { // Skip the requested method - we already tried this one if method == reqMethod || method == http.MethodOptions { continue } handle, _, _ := r.trees[method].getValue(path, nil) if handle != nil { // Add request method to list of allowed methods allowed = append(allowed, method) } } } if len(allowed) > 0 { // Add request method to list of allowed methods allowed = append(allowed, http.MethodOptions) // Sort allowed methods. // sort.Strings(allowed) unfortunately causes unnecessary allocations // due to allowed being moved to the heap and interface conversion for i, l := 1, len(allowed); i < l; i++ { for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { allowed[j], allowed[j-1] = allowed[j-1], allowed[j] } } // return as comma separated list return strings.Join(allowed, ", ") } return allow } // ServeHTTP makes the router implement the http.Handler interface. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { if r.PanicHandler != nil { defer r.recv(w, req) } path := req.URL.Path if root := r.trees[req.Method]; root != nil { if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { if ps != nil { handle(w, req, *ps) r.putParams(ps) } else { handle(w, req, nil) } return } else if req.Method != http.MethodConnect && path != "/" { // Moved Permanently, request with GET method code := http.StatusMovedPermanently if req.Method != http.MethodGet { // Permanent Redirect, request with same method code = http.StatusPermanentRedirect } if tsr && r.RedirectTrailingSlash { if len(path) > 1 && path[len(path)-1] == '/' { req.URL.Path = path[:len(path)-1] } else { req.URL.Path = path + "/" } http.Redirect(w, req, req.URL.String(), code) return } // Try to fix the request path if r.RedirectFixedPath { fixedPath, found := root.findCaseInsensitivePath( CleanPath(path), r.RedirectTrailingSlash, ) if found { req.URL.Path = fixedPath http.Redirect(w, req, req.URL.String(), code) return } } } } if req.Method == http.MethodOptions && r.HandleOPTIONS { // Handle OPTIONS requests if allow := r.allowed(path, http.MethodOptions); allow != "" { w.Header().Set("Allow", allow) if r.GlobalOPTIONS != nil { r.GlobalOPTIONS.ServeHTTP(w, req) } return } } else if r.HandleMethodNotAllowed { // Handle 405 if allow := r.allowed(path, req.Method); allow != "" { w.Header().Set("Allow", allow) if r.MethodNotAllowed != nil { r.MethodNotAllowed.ServeHTTP(w, req) } else { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed, ) } return } } // Handle 404 if r.NotFound != nil { r.NotFound.ServeHTTP(w, req) } else { http.NotFound(w, req) } }