1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-22 06:52:46 +00:00
fiber/router.go
2020-02-29 22:11:25 -05:00

420 lines
10 KiB
Go

// 🚀 Fiber is an Express inspired web framework written in Go with 💖
// 📌 API Documentation: https://fiber.wiki
// 📝 Github Repository: https://github.com/gofiber/fiber
package fiber
import (
"fmt"
"log"
"path/filepath"
"regexp"
"strings"
websocket "github.com/fasthttp/websocket"
fasthttp "github.com/valyala/fasthttp"
)
// Route struct
type Route struct {
// HTTP method in uppercase, can be a * for "Use" & "All" routes
Method string
// Stores the original path
Path string
// Prefix is for ending wildcards or middleware
Prefix string
// Stores regex for complex paths
Regex *regexp.Regexp
// Stores params keys for :params / :optional? / *
Params []string
// Handler for context
HandlerCtx func(*Ctx)
// Handler for websockets
HandlerConn func(*Conn)
}
func (app *App) registerStatic(grpPrefix string, args ...string) {
var prefix = "/"
var root = "./"
// enable / disable gzipping somewhere?
// todo v2.0.0
gzip := true
if len(args) == 1 {
root = args[0]
}
if len(args) == 2 {
prefix = args[0]
root = args[1]
}
// A non wildcard path must start with a '/'
if prefix != "*" && len(prefix) > 0 && prefix[0] != '/' {
prefix = "/" + prefix
}
// Prepend group prefix
if len(grpPrefix) > 0 {
// `/v1`+`/` => `/v1`+``
if prefix == "/" {
prefix = grpPrefix
} else {
prefix = grpPrefix + prefix
}
// Remove duplicate slashes `//`
prefix = strings.Replace(prefix, "//", "/", -1)
}
// Empty or '/*' path equals "match anything"
// TODO fix * for paths with grpprefix
if prefix == "/*" {
prefix = "*"
}
// Lets get all files from root
files, _, err := getFiles(root)
if err != nil {
log.Fatal("Static: ", err)
}
// ./static/compiled => static/compiled
mount := filepath.Clean(root)
if !app.Settings.CaseSensitive {
prefix = strings.ToLower(prefix)
}
if !app.Settings.StrictRouting && len(prefix) > 1 {
prefix = strings.TrimRight(prefix, "/")
}
// Loop over all files
for _, file := range files {
// Ignore the .gzipped files by fasthttp
if strings.Contains(file, ".fasthttp.gz") {
continue
}
// Time to create a fake path for the route match
// static/index.html => /index.html
path := filepath.Join(prefix, strings.Replace(file, mount, "", 1))
// for windows: static\index.html => /index.html
path = filepath.ToSlash(path)
// Store file path to use in ctx handler
filePath := file
if len(prefix) > 1 && strings.Contains(prefix, "*") {
app.routes = append(app.routes, &Route{
Method: "GET",
Path: path,
Prefix: strings.Split(prefix, "*")[0],
HandlerCtx: func(c *Ctx) {
c.SendFile(filePath, gzip)
},
})
return
}
// If the file is an index.html, bind the prefix to index.html directly
if filepath.Base(filePath) == "index.html" || filepath.Base(filePath) == "index.htm" {
app.routes = append(app.routes, &Route{
Method: "GET",
Path: prefix,
HandlerCtx: func(c *Ctx) {
c.SendFile(filePath, gzip)
},
})
}
if !app.Settings.CaseSensitive {
path = strings.ToLower(path)
}
if !app.Settings.StrictRouting && len(prefix) > 1 {
path = strings.TrimRight(path, "/")
}
// Add the route + SendFile(filepath) to routes
app.routes = append(app.routes, &Route{
Method: "GET",
Path: path,
HandlerCtx: func(c *Ctx) {
c.SendFile(filePath, gzip)
},
})
}
}
func (app *App) registerWebSocket(method, group, path string, handler func(*Conn)) {
if len(path) > 0 && path[0] != '/' {
path = "/" + path
}
if len(group) > 0 {
// `/v1`+`/` => `/v1`+``
if path == "/" {
path = group
} else {
path = group + path
}
// Remove duplicate slashes `//`
path = strings.Replace(path, "//", "/", -1)
}
// Routes are case insensitive by default
if !app.Settings.CaseSensitive {
path = strings.ToLower(path)
}
if !app.Settings.StrictRouting && len(path) > 1 {
path = strings.TrimRight(path, "/")
}
// Get ':param' & ':optional?' & '*' from path
params := getParams(path)
if len(params) > 0 {
log.Fatal("WebSocket routes do not support path parameters: `:param, :optional?, *`")
}
app.routes = append(app.routes, &Route{
Method: method, Path: path, HandlerConn: handler,
})
}
func (app *App) registerMethod(method, group, path string, handlers ...func(*Ctx)) {
// No special paths for websockets
if len(handlers) == 0 {
log.Fatalf("Missing handler in route")
}
// Set variables
var prefix string
var middleware = method == "USE"
// A non wildcard path must start with a '/'
if path != "*" && len(path) > 0 && path[0] != '/' {
path = "/" + path
}
// Prepend group prefix
if len(group) > 0 {
// `/v1`+`/` => `/v1`+``
if path == "/" {
path = group
} else {
path = group + path
}
// Remove duplicate slashes `//`
path = strings.Replace(path, "//", "/", -1)
}
// Empty or '/*' path equals "match anything"
// TODO fix * for paths with grpprefix
if path == "/*" || (middleware && path == "") {
path = "*"
}
if method == "ALL" || middleware {
method = "*"
}
// Routes are case insensitive by default
if !app.Settings.CaseSensitive {
path = strings.ToLower(path)
}
if !app.Settings.StrictRouting && len(path) > 1 {
path = strings.TrimRight(path, "/")
}
// If the route can match anything
if path == "*" {
for i := range handlers {
app.routes = append(app.routes, &Route{
Method: method, Path: path, HandlerCtx: handlers[i],
})
}
return
}
// Get ':param' & ':optional?' & '*' from path
params := getParams(path)
// Enable prefix for midware
if len(params) == 0 && middleware {
prefix = path
}
// If path has no params (simple path)
if len(params) == 0 {
for i := range handlers {
app.routes = append(app.routes, &Route{
Method: method, Path: path, Prefix: prefix, HandlerCtx: handlers[i],
})
}
return
}
// If path only contains 1 wildcard, we can create a prefix
// If its a middleware, we also create a prefix
if len(params) == 1 && params[0] == "*" {
prefix = strings.Split(path, "*")[0]
for i := range handlers {
app.routes = append(app.routes, &Route{
Method: method, Path: path, Prefix: prefix,
Params: params, HandlerCtx: handlers[i],
})
}
return
}
// We have an :param or :optional? and need to compile a regex struct
regex, err := getRegex(path)
if err != nil {
log.Fatal("Router: Invalid path pattern: " + path)
}
// Add route with regex
for i := range handlers {
app.routes = append(app.routes, &Route{
Method: method, Path: path, Regex: regex,
Params: params, HandlerCtx: handlers[i],
})
}
}
func (app *App) handler(fctx *fasthttp.RequestCtx) {
// Use this boolean to perform 404 not found at the end
var match = false
// get custom context from sync pool
ctx := acquireCtx()
defer releaseCtx(ctx)
ctx.app = app
ctx.Fasthttp = fctx
ctx.compress = app.Settings.Compression
// get path and method
path := ctx.Path()
if !app.Settings.CaseSensitive {
path = strings.ToLower(path)
}
if !app.Settings.StrictRouting && len(path) > 1 {
path = strings.TrimRight(path, "/")
}
method := ctx.Method()
// enable recovery
if app.recover != nil {
defer func() {
if r := recover(); r != nil {
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
ctx.error = err
app.recover(ctx)
}
}()
}
// loop trough routes
for _, route := range app.routes {
// Skip route if method does not match
if route.Method != "*" && route.Method != method {
continue
}
// Set route pointer if user wants to call .Route()
ctx.route = route
// wilcard or exact same path
// TODO v2: enable or disable case insensitive match
if route.Path == "*" || route.Path == path {
// if * always set the path to the wildcard parameter
if route.Path == "*" {
ctx.params = &[]string{"*"}
ctx.values = []string{path}
}
// ctx.Fasthttp.Request.Header.ConnectionUpgrade()
// Websocket request
if route.HandlerConn != nil && websocket.FastHTTPIsWebSocketUpgrade(fctx) {
// Try to upgrade
err := socketUpgrade.Upgrade(ctx.Fasthttp, func(fconn *websocket.Conn) {
conn := acquireConn(fconn)
defer releaseConn(conn)
route.HandlerConn(conn)
})
// Upgrading failed
if err != nil {
log.Fatalf("Failed to upgrade websocket connection")
}
return
}
// No handler for HTTP nor websocket
if route.HandlerCtx == nil {
continue
}
// Match found, 404 not needed
match = true
route.HandlerCtx(ctx)
// if next is not set, leave loop and release ctx
if !ctx.next {
break
} else {
// reset match to false
match = false
}
// set next to false for next iteration
ctx.next = false
// continue to go to the next route
continue
}
if route.Prefix != "" && strings.HasPrefix(path, route.Prefix) {
ctx.route = route
if strings.Contains(route.Path, "*") {
ctx.params = &[]string{"*"}
// ctx.values = matches[0][1:len(matches[0])]
// parse query source
ctx.values = []string{strings.Replace(path, route.Prefix, "", 1)}
}
// No handler for HTTP nor websocket
if route.HandlerCtx == nil {
continue
}
// Match found, 404 not needed
match = true
route.HandlerCtx(ctx)
// if next is not set, leave loop and release ctx
if !ctx.next {
break
} else {
// reset match to false
match = false
}
// set next to false for next iteration
ctx.next = false
// continue to go to the next route
continue
}
// Skip route if regex does not exist
if route.Regex == nil {
continue
}
// Skip route if regex does not match
if !route.Regex.MatchString(path) {
continue
}
// If we have parameters, lets find the matches
if len(route.Params) > 0 {
matches := route.Regex.FindAllStringSubmatch(path, -1)
// If we have matches, add params and values to context
if len(matches) > 0 && len(matches[0]) > 1 {
ctx.params = &route.Params
ctx.values = matches[0][1:len(matches[0])]
}
}
// No handler for HTTP nor websocket
if route.HandlerCtx == nil {
continue
}
// Match found, 404 not needed
match = true
route.HandlerCtx(ctx)
// if next is not set, leave loop and release ctx
if !ctx.next {
break
} else {
// reset match to false
match = false
}
// set next to false for next iteration
ctx.next = false
}
// Do we need to compress?
if ctx.compress {
compressDefaultCompression(fctx)
// switch app.Settings.CompressionLevel {
// case 2:
// compressBestSpeed(fctx)
// case 3:
// compressBestCompression(fctx)
// case 4:
// compressHuffmanOnly(fctx)
// default: // 1
// compressDefaultCompression(fctx)
// }
}
// No match, send default 404 Not Found
if !match {
ctx.SendStatus(404)
}
}