diff --git a/go.mod b/go.mod index 9ae62c0..7531a6d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( git.loafle.net/loafer/annotation-go v0.0.0-20191113141909-a254c5695d3b git.loafle.net/loafer/di-go v0.0.0-20191113141126-d08fa9dbe72d - github.com/buaazp/fasthttprouter v0.1.1 + github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 + github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.6.0 ) diff --git a/go.sum b/go.sum index da773da..9cb6e00 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,12 @@ git.loafle.net/loafer/util-go v0.0.0-20191112142134-9a567d18b779 h1:TcBAz07pghDj git.loafle.net/loafer/util-go v0.0.0-20191112142134-9a567d18b779/go.mod h1:HGVw9FNJIc/UFDIzxmoIj5K2+D9Eadal5jjHOq0NFOU= git.loafle.net/loafer/util-go v0.0.0-20191113132317-6eeae49d258d h1:ESDbDHHzH2Ysq+thQrO/OQtyDkVhzNzshjn0SJIqa0g= git.loafle.net/loafer/util-go v0.0.0-20191113132317-6eeae49d258d/go.mod h1:HGVw9FNJIc/UFDIzxmoIj5K2+D9Eadal5jjHOq0NFOU= -github.com/buaazp/fasthttprouter v0.1.1 h1:4oAnN0C3xZjylvZJdP35cxfclyn4TYkW6Y+DSvS+h8Q= -github.com/buaazp/fasthttprouter v0.1.1/go.mod h1:h/Ap5oRVLeItGKTVBb+heQPks+HdIUtGmI4H5WCYijM= github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 h1:9Pi10H7E8E79/x2HSe1FmMGd7BJ1WAqDKzwjpv+ojFg= +github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= diff --git a/pkg/loafer/app/app.go b/pkg/loafer/app/app.go index ca38a1c..c27cd5f 100644 --- a/pkg/loafer/app/app.go +++ b/pkg/loafer/app/app.go @@ -8,7 +8,7 @@ import ( "git.loafle.net/loafer/di-go" appAnnotation "git.loafle.net/totopia/server/pkg/loafer/app/annotation" webAnnotation "git.loafle.net/totopia/server/pkg/loafer/web/annotation" - "github.com/buaazp/fasthttprouter" + "git.loafle.net/totopia/server/pkg/loafer/web/router" "github.com/valyala/fasthttp" ) @@ -31,56 +31,48 @@ func Run(t reflect.Type) error { return fmt.Errorf("[%v]", err) } + r := router.New() + for _, restHandler := range restHandlers { - parseRestHandler(restHandler) - parseRequestMapping(restHandler) + rha := parseRestHandler(restHandler) + parseRequestMapping(rha, r, restHandler) } - router := fasthttprouter.New() - if err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", aServer.HTTPPort), router.Handler); nil != err { + if err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", aServer.HTTPPort), r.Handler); nil != err { return err } return nil } -func parseRestHandler(restHandler interface{}) { +func parseRestHandler(restHandler interface{}) *webAnnotation.RestHandlerAnnotation { t := reflect.TypeOf(restHandler) ta := di.GetTypeAnnotation(t, webAnnotation.RestHandlerAnnotationType) if nil == ta { log.Printf("Service[%s] is not RESTService, use @RESTService", t.Elem().Name()) - return + return nil } log.Printf("%s %v", t.Elem().Name(), ta) + return ta.(*webAnnotation.RestHandlerAnnotation) } -func parseRequestMapping(restHandler interface{}) { +func parseRequestMapping(rha *webAnnotation.RestHandlerAnnotation, r *router.Router, restHandler interface{}) { t := reflect.TypeOf(restHandler) mas := di.GetMethodAnnotations(t, webAnnotation.RequestMappingAnnotationType) if nil == mas || 0 == len(mas) { return } - methodMapping := make(map[string]map[string]*MethodMapping) - - for methodName, v := range mas { - ma := v.(*webAnnotation.RequestMappingAnnotation) - mm, ok := methodMapping[ma.Method] - if !ok { - mm = make(map[string]*MethodMapping) - methodMapping[ma.Method] = mm - } - - _, ok = mm[ma.Entry] - if ok { - log.Printf("Mapping of method[%s], entry[%s] is exist already", ma.Method, ma.Entry) - continue - } - - mm[ma.Entry] = &MethodMapping{ - Method: fmt.Sprintf("%s.%s", t.Elem().Name(), methodName), + mf := func(rh interface{}, mn string) fasthttp.RequestHandler { + return func(ctx *fasthttp.RequestCtx) { + reflect.ValueOf(rh).MethodByName(mn).Call([]reflect.Value{reflect.ValueOf(ctx)}) } } - log.Printf("%s %v", t.Elem().Name(), methodMapping) + for methodName, v := range mas { + ma := v.(*webAnnotation.RequestMappingAnnotation) + entry := fmt.Sprintf("%s%s", rha.Entry, ma.Entry) + log.Printf("methodName %s entry %s", methodName, entry) + r.Handle(ma.Method, entry, mf(restHandler, methodName)) + } } diff --git a/pkg/loafer/web/router/path.go b/pkg/loafer/web/router/path.go new file mode 100644 index 0000000..62cc741 --- /dev/null +++ b/pkg/loafer/web/router/path.go @@ -0,0 +1,171 @@ +package router + +import ( + "strings" + "sync" + + "github.com/savsgio/gotils" +) + +type cleanPathBuffer struct { + n int + r int + w int + trailing bool + buf []byte +} + +var cleanPathBufferPool = sync.Pool{ + New: func() interface{} { + return &cleanPathBuffer{ + n: 0, + r: 0, + w: 1, + trailing: false, + buf: make([]byte, 140), + } + }, +} + +func (cpb *cleanPathBuffer) reset() { + cpb.n = 0 + cpb.r = 0 + cpb.w = 1 + cpb.trailing = false + // cpb.buf = cpb.buf[:0] +} + +func acquireCleanPathBuffer() *cleanPathBuffer { + return cleanPathBufferPool.Get().(*cleanPathBuffer) +} + +func releaseCleanPathBuffer(cpb *cleanPathBuffer) { + cpb.reset() + cleanPathBufferPool.Put(cpb) +} + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for path, 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(path string) string { + cpb := acquireCleanPathBuffer() + cleanPathWithBuffer(cpb, path) + + s := string(cpb.buf) + releaseCleanPathBuffer(cpb) + + return s +} + +func cleanPathWithBuffer(cpb *cleanPathBuffer, path string) { + // Turn empty string into "/" + if path == "" { + cpb.buf = append(cpb.buf[:0], '/') + return + } + + cpb.n = len(path) + cpb.buf = gotils.ExtendByteSlice(cpb.buf, len(path)+1) + cpb.buf[0] = '/' + + cpb.trailing = cpb.n > 2 && path[cpb.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 cpb.r < cpb.n { + // println(path[:cpb.r], " ####### ", string(path[cpb.r]), " ####### ", string(cpb.buf)) + switch { + case path[cpb.r] == '/': + // empty path element, trailing slash is added after the end + cpb.r++ + + case path[cpb.r] == '.' && cpb.r+1 == cpb.n: + cpb.trailing = true + cpb.r++ + + case path[cpb.r] == '.' && path[cpb.r+1] == '/': + // . element + cpb.r++ + + case path[cpb.r] == '.' && path[cpb.r+1] == '.' && (cpb.r+2 == cpb.n || path[cpb.r+2] == '/'): + // .. element: remove to last / + cpb.r += 2 + + if cpb.w > 1 { + // can backtrack + cpb.w-- + + for cpb.w > 1 && cpb.buf[cpb.w] != '/' { + cpb.w-- + } + + } + + default: + // real path element. + // add slash if needed + if cpb.w > 1 { + cpb.buf[cpb.w] = '/' + cpb.w++ + } + + // copy element + for cpb.r < cpb.n && path[cpb.r] != '/' { + cpb.buf[cpb.w] = path[cpb.r] + cpb.w++ + cpb.r++ + } + } + } + + // re-append trailing slash + if cpb.trailing && cpb.w > 1 { + cpb.buf[cpb.w] = '/' + cpb.w++ + } + + cpb.buf = cpb.buf[:cpb.w] +} + +// returns all possible paths when the original path has optional arguments +func getOptionalPaths(path string) []string { + paths := make([]string, 0) + + index := 0 + newParam := false + for i := 0; i < len(path); i++ { + c := path[i] + + if c == ':' { + index = i + newParam = true + } else if i > 0 && newParam && c == '?' { + p := strings.Replace(path[:index], "?", "", -1) + p = p[:len(p)-1] + if !gotils.StringSliceInclude(paths, p) { + paths = append(paths, p) + } + + p = strings.Replace(path[:i], "?", "", -1) + if !gotils.StringSliceInclude(paths, p) { + paths = append(paths, p) + } + + newParam = false + } + } + + return paths +} diff --git a/pkg/loafer/web/router/router.go b/pkg/loafer/web/router/router.go new file mode 100644 index 0000000..a290289 --- /dev/null +++ b/pkg/loafer/web/router/router.go @@ -0,0 +1,466 @@ +// Package router is a trie based high performance HTTP request router. +// +// A trivial example is: +// +// package main + +// import ( +// "fmt" +// "log" +// +// "github.com/fasthttp/router" +// "github.com/valyala/fasthttp" +// ) + +// func Index(ctx *fasthttp.RequestCtx) { +// fmt.Fprint(ctx, "Welcome!\n") +// } + +// func Hello(ctx *fasthttp.RequestCtx) { +// fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) +// } + +// func main() { +// r := router.New() +// r.GET("/", Index) +// g := r.Group("/foo", Index) +// g.GET("/bar", Index) +// r.GET("/hello/:name", Hello) + +// log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) +// } +// +// The router matches incoming requests by the request method and the path. +// If a handle is registered for this path and method, the router delegates the +// request to that function. +// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to +// register handles, for all other methods router.Handle can be used. +// +// The registered path, against which the router matches incoming requests, can +// contain two types of parameters: +// Syntax Type +// :name named parameter +// *name catch-all parameter +// +// Named parameters are dynamic path segments. They match anything until the +// next '/' or the path end: +// Path: /blog/:category/:post +// +// Requests: +// /blog/go/request-routers match: category="go", post="request-routers" +// /blog/go/request-routers/ no match, but the router would redirect +// /blog/go/ no match +// /blog/go/request-routers/comments no match +// +// Catch-all parameters match anything until the path end, including the +// directory index (the '/' before the catch-all). Since they match anything +// until the end, catch-all parameters must always be the final path element. +// Path: /files/*filepath +// +// Requests: +// /files/ match: filepath="/" +// /files/LICENSE match: filepath="/LICENSE" +// /files/templates/article.html match: filepath="/templates/article.html" +// /files no match, but the router would redirect +// +// The value of parameters is inside ctx.UserValue +// To retrieve the value of a parameter: +// // use the name of the parameter +// user := ps.UserValue("user") +// + +package router + +import ( + "strings" + + "github.com/savsgio/gotils" + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +var ( + defaultContentType = []byte("text/plain; charset=utf-8") + questionMark = []byte("?") +) + +// Router is a http.Handler which can be used to dispatch requests to different +// handler functions via configurable routes +type Router struct { + parent *Router + beginPath string + trees map[string]*node + registeredPaths map[string][]string + + // 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 + + // If enabled, the router automatically replies to OPTIONS requests. + // Custom OPTIONS handlers take priority over automatic replies. + HandleOPTIONS bool + + // Configurable http.Handler which is called when no matching route is + // found. If it is not set, http.NotFound is used. + NotFound fasthttp.RequestHandler + + // 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 fasthttp.RequestHandler + + // 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(*fasthttp.RequestCtx, interface{}) +} + +// New returns a new initialized Router. +// Path auto-correction, including trailing slashes, is enabled by default. +func New() *Router { + return &Router{ + beginPath: "/", + trees: make(map[string]*node), + registeredPaths: make(map[string][]string), + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, + HandleOPTIONS: true, + } +} + +// Group returns a new grouped Router. +// Path auto-correction, including trailing slashes, is enabled by default. +func (r *Router) Group(path string) *Router { + g := New() + g.parent = r + g.beginPath = path + + return g +} + +// GET is a shortcut for router.Handle("GET", path, handle) +func (r *Router) GET(path string, handle fasthttp.RequestHandler) { + r.Handle("GET", path, handle) +} + +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func (r *Router) HEAD(path string, handle fasthttp.RequestHandler) { + r.Handle("HEAD", path, handle) +} + +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func (r *Router) OPTIONS(path string, handle fasthttp.RequestHandler) { + r.Handle("OPTIONS", path, handle) +} + +// POST is a shortcut for router.Handle("POST", path, handle) +func (r *Router) POST(path string, handle fasthttp.RequestHandler) { + r.Handle("POST", path, handle) +} + +// PUT is a shortcut for router.Handle("PUT", path, handle) +func (r *Router) PUT(path string, handle fasthttp.RequestHandler) { + r.Handle("PUT", path, handle) +} + +// PATCH is a shortcut for router.Handle("PATCH", path, handle) +func (r *Router) PATCH(path string, handle fasthttp.RequestHandler) { + r.Handle("PATCH", path, handle) +} + +// DELETE is a shortcut for router.Handle("DELETE", path, handle) +func (r *Router) DELETE(path string, handle fasthttp.RequestHandler) { + r.Handle("DELETE", path, 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 fasthttp.RequestHandler) { + if path[0] != '/' { + panic("path must begin with '/' in path '" + path + "'") + } + + if r.beginPath != "/" { + path = r.beginPath + path + } + + r.registeredPaths[method] = append(r.registeredPaths[method], path) + + // Call to the parent recursively until main router to register paths in it + if r.parent != nil { + r.parent.Handle(method, path, handle) + return + } + + root := r.trees[method] + if root == nil { + root = new(node) + r.trees[method] = root + } + + optionalPaths := getOptionalPaths(path) + + // if not has optional paths, adds the original + if len(optionalPaths) == 0 { + root.addRoute(path, handle) + } else { + for _, p := range optionalPaths { + root.addRoute(p, handle) + } + } +} + +// 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. +// router.ServeFiles("/src/*filepath", "/var/www") +func (r *Router) ServeFiles(path string, rootPath string) { + if len(path) < 10 || path[len(path)-10:] != "/*filepath" { + panic("path must end with /*filepath in path '" + path + "'") + } + + if r.beginPath != "/" { + path = r.beginPath + path + } + + if r.parent != nil { + r.parent.ServeFiles(path, rootPath) + return + } + + prefix := path[:len(path)-10] + fileHandler := fasthttp.FSHandler(rootPath, strings.Count(prefix, "/")) + + r.GET(path, func(ctx *fasthttp.RequestCtx) { + fileHandler(ctx) + }) +} + +// ServeFilesCustom serves files from the given file system settings. +// 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. +// router.ServeFilesCustom("/src/*filepath", *customFS) +func (r *Router) ServeFilesCustom(path string, fs *fasthttp.FS) { + if len(path) < 10 || path[len(path)-10:] != "/*filepath" { + panic("path must end with /*filepath in path '" + path + "'") + } + + if r.beginPath != "/" { + path = r.beginPath + path + } + + if r.parent != nil { + r.parent.ServeFilesCustom(path, fs) + return + } + + prefix := path[:len(path)-10] + stripSlashes := strings.Count(prefix, "/") + + if fs.PathRewrite == nil && stripSlashes > 0 { + fs.PathRewrite = fasthttp.NewPathSlashesStripper(stripSlashes) + } + fileHandler := fs.NewRequestHandler() + + r.GET(path, func(ctx *fasthttp.RequestCtx) { + fileHandler(ctx) + }) +} + +// Handler makes the router implement the fasthttp.ListenAndServe interface. +func (r *Router) Handler(ctx *fasthttp.RequestCtx) { + if r.PanicHandler != nil { + defer r.recv(ctx) + } + + path := gotils.B2S(ctx.Path()) + method := gotils.B2S(ctx.Method()) + + if root := r.trees[method]; root != nil { + if f, tsr := root.getValue(path, ctx); f != nil { + f(ctx) + return + } else if method != "CONNECT" && path != "/" { + code := 301 // Permanent redirect, request with GET method + if method != "GET" { + // Temporary redirect, request with same method + // As of Go 1.3, Go does not support status code 308. + code = 307 + } + + if tsr && r.RedirectTrailingSlash { + uri := bytebufferpool.Get() + + if len(path) > 1 && path[len(path)-1] == '/' { + uri.SetString(path[:len(path)-1]) + } else { + uri.SetString(path) + uri.WriteString("/") + } + + if len(ctx.URI().QueryString()) > 0 { + uri.WriteString("?") + uri.Write(ctx.QueryArgs().QueryString()) + } + + ctx.Redirect(uri.String(), code) + + bytebufferpool.Put(uri) + + return + } + + // Try to fix the request path + if r.RedirectFixedPath { + cpb := acquireCleanPathBuffer() + cleanPathWithBuffer(cpb, path) + fixedPath, found := root.findCaseInsensitivePath(gotils.B2S(cpb.buf), r.RedirectTrailingSlash) + releaseCleanPathBuffer(cpb) + + if found { + queryBuf := ctx.URI().QueryString() + if len(queryBuf) > 0 { + fixedPath = append(fixedPath, questionMark...) + fixedPath = append(fixedPath, queryBuf...) + } + ctx.RedirectBytes(fixedPath, code) + return + } + } + } + } + + if method == "OPTIONS" { + // Handle OPTIONS requests + if r.HandleOPTIONS { + if allow := r.allowed(path, method); len(allow) > 0 { + ctx.Response.Header.Set("Allow", allow) + return + } + } + } else { + // Handle 405 + if r.HandleMethodNotAllowed { + if allow := r.allowed(path, method); len(allow) > 0 { + ctx.Response.Header.Set("Allow", allow) + if r.MethodNotAllowed != nil { + r.MethodNotAllowed(ctx) + } else { + ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) + ctx.SetContentTypeBytes(defaultContentType) + ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed)) + } + return + } + } + } + + // Handle 404 + if r.NotFound != nil { + r.NotFound(ctx) + } else { + ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound), fasthttp.StatusNotFound) + } +} + +// 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, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) { + if root := r.trees[method]; root != nil { + return root.getValue(path, ctx) + } + return nil, false +} + +// List returns all registered routes grouped by method +func (r *Router) List() map[string][]string { + return r.registeredPaths +} + +func (r *Router) allowed(path, reqMethod string) (allow string) { + if path == "*" || path == "/*" { // server-wide + for method := range r.trees { + if method == "OPTIONS" { + continue + } + + // add request method to list of allowed methods + if len(allow) == 0 { + allow = method + } else { + allow += ", " + method + } + } + } else { // specific path + for method := range r.trees { + // Skip the requested method - we already tried this one + if method == reqMethod || method == "OPTIONS" { + continue + } + + handle, _ := r.trees[method].getValue(path, nil) + if handle != nil { + // add request method to list of allowed methods + if len(allow) == 0 { + allow = method + } else { + allow += ", " + method + } + } + } + } + if len(allow) > 0 { + allow += ", OPTIONS" + } + return +} + +func (r *Router) recv(ctx *fasthttp.RequestCtx) { + if rcv := recover(); rcv != nil { + r.PanicHandler(ctx, rcv) + } +} diff --git a/pkg/loafer/web/router/tolower.go b/pkg/loafer/web/router/tolower.go new file mode 100644 index 0000000..1a67b82 --- /dev/null +++ b/pkg/loafer/web/router/tolower.go @@ -0,0 +1,99 @@ +package router + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// toLower returns a copy of the string s with all Unicode letters mapped to their lower case. +func toLower(s string) string { + isASCII, hasUpper := true, false + for i := 0; i < len(s); i++ { + c := s[i] + if c >= utf8.RuneSelf { + isASCII = false + break + } + hasUpper = hasUpper || (c >= 'A' && c <= 'Z') + } + + if isASCII { // optimize for ASCII-only strings. + if !hasUpper { + return s + } + var b strings.Builder + b.Grow(len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + b.WriteByte(c) + } + return b.String() + } + return stringsMap(unicode.ToLower, s) +} + +// Map returns a copy of the string s with all its characters modified +// according to the mapping function. If mapping returns a negative value, the character is +// dropped from the string with no replacement. +func stringsMap(mapping func(rune) rune, s string) string { + // In the worst case, the string can grow when mapped, making + // things unpleasant. But it's so rare we barge in assuming it's + // fine. It could also shrink but that falls out naturally. + + // The output buffer b is initialized on demand, the first + // time a character differs. + var b strings.Builder + + for i, c := range s { + r := mapping(c) + if r == c { + continue + } + + b.Grow(len(s) + utf8.UTFMax) + b.WriteString(s[:i]) + if r >= 0 { + b.WriteRune(r) + } + + if c == utf8.RuneError { + // RuneError is the result of either decoding + // an invalid sequence or '\uFFFD'. Determine + // the correct number of bytes we need to advance. + _, w := utf8.DecodeRuneInString(s[i:]) + i += w + } else { + i += utf8.RuneLen(c) + } + + s = s[i:] + break + } + + // Fast path for unchanged input + if b.Cap() == 0 { // didn't call b.Grow above + return s + } + + for _, c := range s { + r := mapping(c) + + if r >= 0 { + // common case + // Due to inlining, it is more performant to determine if WriteByte should be + // invoked rather than always call WriteRune + if r < utf8.RuneSelf { + b.WriteByte(byte(r)) + } else { + // r is not a ASCII rune. + b.WriteRune(r) + } + } + } + + return b.String() +} diff --git a/pkg/loafer/web/router/tree.go b/pkg/loafer/web/router/tree.go new file mode 100644 index 0000000..b0de088 --- /dev/null +++ b/pkg/loafer/web/router/tree.go @@ -0,0 +1,670 @@ +package router + +import ( + "strings" + "sync" + "unicode" + "unicode/utf8" + + "github.com/savsgio/gotils" + "github.com/valyala/fasthttp" +) + +const ( + static nodeType = iota // default + root + param + catchAll +) + +type nodeType uint8 + +type buffer struct { + b []byte +} + +type node struct { + path string + wildChild bool + nType nodeType + maxParams uint8 + indices string + children []*node + handle fasthttp.RequestHandler + priority uint32 +} + +var bufferPool = sync.Pool{ + New: func() interface{} { + return &buffer{ + b: make([]byte, 0, 0), + } + }, +} + +func acquireBuffer() *buffer { + return bufferPool.Get().(*buffer) +} + +func releaseBuffer(b *buffer) { + bufferPool.Put(b) +} + +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) +} + +// shift bytes in array by n bytes left +func shiftNRuneBytes(rb [4]byte, n int) [4]byte { + switch n { + case 0: + return rb + case 1: + return [4]byte{rb[1], rb[2], rb[3], 0} + case 2: + return [4]byte{rb[2], rb[3]} + case 3: + return [4]byte{rb[3]} + default: + return [4]byte{} + } +} + +// 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, handle fasthttp.RequestHandler) { + fullPath := path + 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, + nType: static, + indices: n.indices, + children: n.children, + handle: n.handle, + 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.handle = 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 + (len(n.path) >= len(path) || path[len(n.path)] == '/') { + continue walk + } else { + // Wildcard conflict + pathSeg := strings.SplitN(path, "/", 2)[0] + prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path + panic("'" + pathSeg + + "' in new path '" + fullPath + + "' conflicts with existing wildcard '" + n.path + + "' in existing prefix '" + prefix + + "'") + } + } + + 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, fullPath, handle) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handle != nil { + panic("a handle is already registered for path '" + fullPath + "'") + } + n.handle = handle + } + return + } + } else { // Empty tree + n.insertChild(numParams, path, fullPath, handle) + n.nType = root + } +} + +func (n *node) insertChild(numParams uint8, path, fullPath string, handle fasthttp.RequestHandler) { + 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 + } + + // 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, has: '" + + path[i:] + "' in path '" + fullPath + "'") + default: + end++ + } + } + + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route '" + path[i:end] + + "' conflicts with existing children in path '" + fullPath + "'") + } + + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") + } + + 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 in path '" + fullPath + "'") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } + + 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, + handle: handle, + priority: 1, + } + n.children = []*node{child} + + return + } + } + + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handle = handle +} + +// 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, ctx *fasthttp.RequestCtx) (handle fasthttp.RequestHandler, 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.handle != 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++ + } + + // handle calls to Router.allowed method with nil context + if ctx != nil { + ctx.SetUserValue(n.path[1:], 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 handle = n.handle; handle != 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.handle != nil) + } + + return + + case catchAll: + if ctx != nil { + // save param value + ctx.SetUserValue(n.path[2:], path) + } + handle = n.handle + 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 handle = n.handle; handle != nil { + return + } + + if path == "/" && n.wildChild && n.nType != root { + tsr = true + 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.handle != nil) || + (n.nType == catchAll && n.children[0].handle != 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.handle != 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) ([]byte, bool) { + buff := acquireBuffer() + + buff.b = gotils.ExtendByteSlice(buff.b, len(path)+1) // preallocate enough memory for new path + + fixedPath, found := n.findCaseInsensitivePathRec( + path, + toLower(path), + buff.b[:0], + [4]byte{}, // empty rune buffer + fixTrailingSlash, + ) + + releaseBuffer(buff) + + return fixedPath, found +} + +// recursive case-insensitive lookup function used by n.findCaseInsensitivePath +func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { + loNPath := toLower(n.path) + +walk: // outer loop for walking the tree + for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) { + // add common path to result + ciPath = append(ciPath, n.path...) + + if path = path[len(n.path):]; len(path) > 0 { + loOld := loPath + loPath = loPath[len(loNPath):] + + // 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 { + // skip rune bytes already processed + rb = shiftNRuneBytes(rb, len(loNPath)) + + if rb[0] != 0 { + // old rune not finished + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == rb[0] { + // continue with child node + n = n.children[i] + loNPath = toLower(n.path) + continue walk + } + } + } else { + // process a new rune + var rv rune + + // find rune start + // runes are up to 4 byte long, + // -4 would definitely be another rune + var off int + for max := min(len(loNPath), 3); off < max; off++ { + if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) { + // read rune from cached lowercase path + rv, _ = utf8.DecodeRuneInString(loOld[i:]) + break + } + } + + // calculate lowercase bytes of current rune + utf8.EncodeRune(rb[:], rv) + // skipp already processed bytes + rb = shiftNRuneBytes(rb, off) + + for i := 0; i < len(n.indices); i++ { + // lowercase matches + if n.indices[i] == rb[0] { + // must use a recursive approach since both the + // uppercase byte and the lowercase byte might exist + // as an index + if out, found := n.children[i].findCaseInsensitivePathRec( + path, loPath, ciPath, rb, fixTrailingSlash, + ); found { + return out, true + } + break + } + } + + // same for uppercase rune, if it differs + if up := unicode.ToUpper(rv); up != rv { + utf8.EncodeRune(rb[:], up) + rb = shiftNRuneBytes(rb, off) + + for i := 0; i < len(n.indices); i++ { + // uppercase matches + if n.indices[i] == rb[0] { + // continue with child node + n = n.children[i] + loNPath = toLower(n.path) + continue walk + } + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil) + } + + 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 { + // continue with child node + n = n.children[0] + loNPath = toLower(n.path) + loPath = loPath[k:] + path = path[k:] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true + } + return ciPath, false + } + + if n.handle != 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.handle != nil { + return append(ciPath, '/'), true + } + } + return ciPath, false + + 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.handle != 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.handle != nil) || + (n.nType == catchAll && n.children[0].handle != nil) { + return append(ciPath, '/'), true + } + return ciPath, false + } + } + } + return ciPath, false + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath, true + } + if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' && + loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil { + return append(ciPath, n.path...), true + } + } + return ciPath, false +} diff --git a/pkg/modules/user/user-handler.go b/pkg/modules/user/user-handler.go index 7c56444..4c14a49 100644 --- a/pkg/modules/user/user-handler.go +++ b/pkg/modules/user/user-handler.go @@ -29,13 +29,13 @@ type UserHandler struct { } func (us *UserHandler) List(ctx *fasthttp.RequestCtx) error { - fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) + fmt.Fprintf(ctx, "List hello, %s!\n", ctx.UserValue("id")) return nil } -func (us *UserHandler) Detail(ctx *fasthttp.RequestCtx, userId string) error { - fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) +func (us *UserHandler) Detail(ctx *fasthttp.RequestCtx) error { + fmt.Fprintf(ctx, "Detail hello, %s!\n", ctx.UserValue("id")) return nil }