This commit is contained in:
병준 박 2019-11-15 00:53:14 +09:00
parent 93e784914b
commit fd35acea89
8 changed files with 1432 additions and 33 deletions

3
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require ( require (
git.loafle.net/loafer/annotation-go v0.0.0-20191113141909-a254c5695d3b git.loafle.net/loafer/annotation-go v0.0.0-20191113141909-a254c5695d3b
git.loafle.net/loafer/di-go v0.0.0-20191113141126-d08fa9dbe72d 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 github.com/valyala/fasthttp v1.6.0
) )

4
go.sum
View File

@ -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-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 h1:ESDbDHHzH2Ysq+thQrO/OQtyDkVhzNzshjn0SJIqa0g=
git.loafle.net/loafer/util-go v0.0.0-20191113132317-6eeae49d258d/go.mod h1:HGVw9FNJIc/UFDIzxmoIj5K2+D9Eadal5jjHOq0NFOU= 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 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 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 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=

View File

@ -8,7 +8,7 @@ import (
"git.loafle.net/loafer/di-go" "git.loafle.net/loafer/di-go"
appAnnotation "git.loafle.net/totopia/server/pkg/loafer/app/annotation" appAnnotation "git.loafle.net/totopia/server/pkg/loafer/app/annotation"
webAnnotation "git.loafle.net/totopia/server/pkg/loafer/web/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" "github.com/valyala/fasthttp"
) )
@ -31,56 +31,48 @@ func Run(t reflect.Type) error {
return fmt.Errorf("[%v]", err) return fmt.Errorf("[%v]", err)
} }
r := router.New()
for _, restHandler := range restHandlers { for _, restHandler := range restHandlers {
parseRestHandler(restHandler) rha := parseRestHandler(restHandler)
parseRequestMapping(restHandler) parseRequestMapping(rha, r, restHandler)
} }
router := fasthttprouter.New() if err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", aServer.HTTPPort), r.Handler); nil != err {
if err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", aServer.HTTPPort), router.Handler); nil != err {
return err return err
} }
return nil return nil
} }
func parseRestHandler(restHandler interface{}) { func parseRestHandler(restHandler interface{}) *webAnnotation.RestHandlerAnnotation {
t := reflect.TypeOf(restHandler) t := reflect.TypeOf(restHandler)
ta := di.GetTypeAnnotation(t, webAnnotation.RestHandlerAnnotationType) ta := di.GetTypeAnnotation(t, webAnnotation.RestHandlerAnnotationType)
if nil == ta { if nil == ta {
log.Printf("Service[%s] is not RESTService, use @RESTService", t.Elem().Name()) log.Printf("Service[%s] is not RESTService, use @RESTService", t.Elem().Name())
return return nil
} }
log.Printf("%s %v", t.Elem().Name(), ta) 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) t := reflect.TypeOf(restHandler)
mas := di.GetMethodAnnotations(t, webAnnotation.RequestMappingAnnotationType) mas := di.GetMethodAnnotations(t, webAnnotation.RequestMappingAnnotationType)
if nil == mas || 0 == len(mas) { if nil == mas || 0 == len(mas) {
return return
} }
methodMapping := make(map[string]map[string]*MethodMapping) mf := func(rh interface{}, mn string) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
reflect.ValueOf(rh).MethodByName(mn).Call([]reflect.Value{reflect.ValueOf(ctx)})
}
}
for methodName, v := range mas { for methodName, v := range mas {
ma := v.(*webAnnotation.RequestMappingAnnotation) ma := v.(*webAnnotation.RequestMappingAnnotation)
mm, ok := methodMapping[ma.Method] entry := fmt.Sprintf("%s%s", rha.Entry, ma.Entry)
if !ok { log.Printf("methodName %s entry %s", methodName, entry)
mm = make(map[string]*MethodMapping) r.Handle(ma.Method, entry, mf(restHandler, methodName))
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),
}
}
log.Printf("%s %v", t.Elem().Name(), methodMapping)
} }

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -29,13 +29,13 @@ type UserHandler struct {
} }
func (us *UserHandler) List(ctx *fasthttp.RequestCtx) error { 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 return nil
} }
func (us *UserHandler) Detail(ctx *fasthttp.RequestCtx, userId string) error { func (us *UserHandler) Detail(ctx *fasthttp.RequestCtx) error {
fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) fmt.Fprintf(ctx, "Detail hello, %s!\n", ctx.UserValue("id"))
return nil return nil
} }