github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/http/fast/router.go (about)

     1  // 🚀 Fast is an Express inspired web framework written in Go.
     2  
     3  package fast
     4  
     5  import (
     6  	"log"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/valyala/fasthttp"
    12  )
    13  
    14  // Route struct
    15  type Route struct {
    16  	isGet bool // allows HEAD requests if GET
    17  
    18  	isMiddleware bool // is middleware route
    19  
    20  	isStar  bool // path == '*'
    21  	isSlash bool // path == '/'
    22  	isRegex bool // needs regex parsing
    23  
    24  	Method string         // http method
    25  	Path   string         // original path
    26  	Params []string       // path params
    27  	Regexp *regexp.Regexp // regexp matcher
    28  
    29  	Handler func(*Ctx) // ctx handler
    30  }
    31  
    32  func (app *Fast) nextRoute(ctx *Ctx) {
    33  	// Keep track of head matches
    34  	l := len(app.routes) - 1
    35  	for ctx.index < l {
    36  		ctx.index++
    37  		route := app.routes[ctx.index]
    38  		match, values := route.matchRoute(ctx.method, ctx.path)
    39  		if match {
    40  			ctx.route = route
    41  			ctx.values = values
    42  			route.Handler(ctx)
    43  			return
    44  		}
    45  	}
    46  	if len(ctx.C.Response.Body()) == 0 {
    47  		ctx.SendStatus(404)
    48  	}
    49  }
    50  
    51  func (r *Route) matchRoute(method, path string) (match bool, values []string) {
    52  	// is route middleware? matches all http methods
    53  	if r.isMiddleware {
    54  		// '*' or '/' means its a valid match
    55  		if r.isStar || r.isSlash {
    56  			return true, values
    57  		}
    58  		// if midware path starts with req.path
    59  		if strings.HasPrefix(path, r.Path) {
    60  			return true, values
    61  		}
    62  		// middlewares dont support regex so bye!
    63  		return false, values
    64  	}
    65  	// non-middleware route, http method must match!
    66  	// the wildcard method is for .All() & .Use() methods
    67  	// If route is GET, also match HEAD requests
    68  	if r.Method == method || r.Method[0] == '*' || (r.isGet && len(method) == 4 && method == "HEAD") {
    69  		// '*' means we match anything
    70  		if r.isStar {
    71  			return true, values
    72  		}
    73  		// simple '/' bool, so you avoid unnecessary comparison for long paths
    74  		if r.isSlash && path == "/" {
    75  			return true, values
    76  		}
    77  		// does this route need regex matching?
    78  		// does req.path match regex pattern?
    79  		if r.isRegex && r.Regexp.MatchString(path) {
    80  			// do we have parameters
    81  			if len(r.Params) > 0 {
    82  				// get values for parameters
    83  				matches := r.Regexp.FindAllStringSubmatch(path, -1)
    84  				// did we get the values?
    85  				if len(matches) > 0 && len(matches[0]) > 1 {
    86  					values = matches[0][1:len(matches[0])]
    87  					return true, values
    88  				}
    89  				return false, values
    90  			}
    91  			return true, values
    92  		}
    93  		// last thing to do is to check for a simple path match
    94  		if len(r.Path) == len(path) && r.Path == path {
    95  			return true, values
    96  		}
    97  	}
    98  	// Nothing match
    99  	return false, values
   100  }
   101  
   102  func (app *Fast) handler(fctx *fasthttp.RequestCtx) {
   103  	// get fiber context from sync pool
   104  	ctx := acquireCtx(fctx)
   105  	defer releaseCtx(ctx)
   106  	// attach app pointer and compress settings
   107  	ctx.app = app
   108  
   109  	// Case sensitive routing
   110  	if !app.Settings.CaseSensitive {
   111  		ctx.path = strings.ToLower(ctx.path)
   112  	}
   113  	// Strict routing
   114  	if !app.Settings.StrictRouting && len(ctx.path) > 1 {
   115  		ctx.path = strings.TrimRight(ctx.path, "/")
   116  	}
   117  	// Find route
   118  	app.nextRoute(ctx)
   119  }
   120  
   121  func (app *Fast) registerMethod(method, path string, handlers ...func(*Ctx)) {
   122  	// Route requires atleast one handler
   123  	if len(handlers) == 0 {
   124  		log.Fatalf("Missing handler in route")
   125  	}
   126  	// Cannot have an empty path
   127  	if path == "" {
   128  		path = "/"
   129  	}
   130  	// Path always start with a '/' or '*'
   131  	if path[0] != '/' && path[0] != '*' {
   132  		path = "/" + path
   133  	}
   134  	// Store original path to strip case sensitive params
   135  	original := path
   136  	// Case sensitive routing, all to lowercase
   137  	if !app.Settings.CaseSensitive {
   138  		path = strings.ToLower(path)
   139  	}
   140  	// Strict routing, remove last `/`
   141  	if !app.Settings.StrictRouting && len(path) > 1 {
   142  		path = strings.TrimRight(path, "/")
   143  	}
   144  	// SetHeader route booleans
   145  	var isGet = method == "GET"
   146  	var isMiddleware = method == "USE"
   147  	// Middleware / All allows all HTTP methods
   148  	if isMiddleware || method == "ALL" {
   149  		method = "*"
   150  	}
   151  	var isStar = path == "*" || path == "/*"
   152  	// Middleware containing only a `/` equals wildcard
   153  	if isMiddleware && path == "/" {
   154  		isStar = true
   155  	}
   156  	var isSlash = path == "/"
   157  	var isRegex = false
   158  	// Route properties
   159  	var Params = getParams(original)
   160  	var Regexp *regexp.Regexp
   161  	// Params requires regex pattern
   162  	if len(Params) > 0 {
   163  		regex, err := getRegex(path)
   164  		if err != nil {
   165  			log.Fatal("Router: Invalid path pattern: " + path)
   166  		}
   167  		isRegex = true
   168  		Regexp = regex
   169  	}
   170  	for i := range handlers {
   171  		app.routes = append(app.routes, &Route{
   172  			isGet:        isGet,
   173  			isMiddleware: isMiddleware,
   174  			isStar:       isStar,
   175  			isSlash:      isSlash,
   176  			isRegex:      isRegex,
   177  			Method:       method,
   178  			Path:         path,
   179  			Params:       Params,
   180  			Regexp:       Regexp,
   181  			Handler:      handlers[i],
   182  		})
   183  	}
   184  }
   185  
   186  func (app *Fast) registerStatic(prefix, root string, config ...Static) {
   187  	// Cannot have an empty prefix
   188  	if prefix == "" {
   189  		prefix = "/"
   190  	}
   191  	// Prefix always start with a '/' or '*'
   192  	if prefix[0] != '/' && prefix[0] != '*' {
   193  		prefix = "/" + prefix
   194  	}
   195  	// Match anything
   196  	var wildcard = false
   197  	if prefix == "*" || prefix == "/*" {
   198  		wildcard = true
   199  		prefix = "/"
   200  	}
   201  	// Case sensitive routing, all to lowercase
   202  	if !app.Settings.CaseSensitive {
   203  		prefix = strings.ToLower(prefix)
   204  	}
   205  	// For security we want to restrict to the current work directory.
   206  	if len(root) == 0 {
   207  		root = "."
   208  	}
   209  	// Strip trailing slashes from the root path
   210  	if len(root) > 0 && root[len(root)-1] == '/' {
   211  		root = root[:len(root)-1]
   212  	}
   213  	// isSlash ?
   214  	var isSlash = prefix == "/"
   215  	if strings.Contains(prefix, "*") {
   216  		wildcard = true
   217  		prefix = strings.Split(prefix, "*")[0]
   218  	}
   219  	var stripper = len(prefix)
   220  	if isSlash {
   221  		stripper = 0
   222  	}
   223  	// File server settings
   224  	fs := &fasthttp.FS{
   225  		Root:                 root,
   226  		GenerateIndexPages:   false,
   227  		AcceptByteRange:      false,
   228  		Compress:             false,
   229  		CompressedFileSuffix: ".gz",
   230  		CacheDuration:        10 * time.Second,
   231  		IndexNames:           []string{"index.html"},
   232  		PathRewrite:          fasthttp.NewPathPrefixStripper(stripper),
   233  		PathNotFound: func(ctx *fasthttp.RequestCtx) {
   234  			ctx.Response.SetStatusCode(404)
   235  			ctx.Response.SetBodyString("Not Found")
   236  		},
   237  	}
   238  	// SetHeader config if provided
   239  	if len(config) > 0 {
   240  		fs.Compress = config[0].Compress
   241  		fs.AcceptByteRange = config[0].ByteRange
   242  		fs.GenerateIndexPages = config[0].Browse
   243  		if config[0].Index != "" {
   244  			fs.IndexNames = []string{config[0].Index}
   245  		}
   246  	}
   247  	fileHandler := fs.NewRequestHandler()
   248  	app.routes = append(app.routes, &Route{
   249  		isMiddleware: true,
   250  		isSlash:      isSlash,
   251  		Method:       "*",
   252  		Path:         prefix,
   253  		Handler: func(c *Ctx) {
   254  			// Only handle GET & HEAD methods
   255  			if c.method == "GET" || c.method == "HEAD" {
   256  				// Do stuff
   257  				if wildcard {
   258  					c.C.Request.SetRequestURI(prefix)
   259  				}
   260  				// Serve file
   261  				fileHandler(c.C)
   262  				// End response when file is found
   263  				if c.C.Response.StatusCode() != 404 {
   264  					return
   265  				}
   266  			}
   267  			c.Next()
   268  		},
   269  	})
   270  }