mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-06 23:31:55 +00:00
ec48a76f14
* Update pull_request_template.md * Update v3-changes.md * Update CONTRIBUTING.md (#2752) Grammar correction. * chore(encryptcookie)!: update default config (#2753) * chore(encryptcookie)!: update default config docs(encryptcookie): enhance documentation and examples BREAKING CHANGE: removed the hardcoded "csrf_" from the Except. * docs(encryptcookie): reads or modifies cookies * chore(encryptcookie): csrf config example * docs(encryptcookie): md table spacing * build(deps): bump actions/setup-go from 4 to 5 (#2754) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 🩹 middleware/logger/: log client IP address by default (#2755) * middleware/logger: Log client IP address by default. * Update doc. * fix: don't constrain middlewares' context-keys to strings 🐛 (#2751) * Revert "Revert "🐛 requestid.Config.ContextKey is interface{} (#2369)" (#2742)" This reverts commit 28be17f929cfa7d3c27dd292fc3956f2f9882e22. * fix: request ContextKey default value condition Should check for `nil` since it is `any`. * fix: don't constrain middlewares' context-keys to strings `context` recommends using "unexported type" as context keys to avoid collisions https://pkg.go.dev/github.com/gofiber/fiber/v2#Ctx.Locals. The official go blog also recommends this https://go.dev/blog/context. `fiber.Ctx.Locals(key any, value any)` correctly allows consumers to use unexported types or e.g. strings. But some fiber middlewares constrain their context-keys to `string` in their "default config structs", making it impossible to use unexported types. This PR removes the `string` _constraint_ from all middlewares, allowing to now use unexported types as per the official guidelines. However the default value is still a string, so it's not a breaking change, and anyone still using strings as context keys is not affected. * 📚 Update app.md for indentation (#2761) Update app.md for indentation * build(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#2762) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github/codeql-action from 2 to 3 (#2763) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Changing default log output (#2730) changing default log output Closes #2729 * Update hooks.md fix wrong hooks signature * 🩹 Fix: CORS middleware should use the defined AllowedOriginsFunc config when AllowedOrigins is empty (#2771) * 🐛 [Bug]: Adaptator + otelfiber issue #2641 (#2772) * 🩹🚨 - fix for redirect with query params (#2748) * redirect with query params did not work, fix it and add test for it * redirect middleware - fix test typo * ♻️ logger/middleware colorize logger error message #2593 (#2773) * ✨ feat: add liveness and readiness checks (#2509) * ✨ feat: add liveness and readiness checkers * 📝 docs: add docs for liveness and readiness * ✨ feat: add options method for probe checkers * ✅ tests: add tests for liveness and readiness * ♻️ refactor: change default endpoint values * ♻️ refactor: change default value for liveness endpoint * 📝 docs: add return status for liveness and readiness probes * ♻️ refactor: change probechecker to middleware * 📝 docs: move docs to middleware session * ♻️ refactor: apply gofumpt formatting * ♻️ refactor: remove unused parameter * split config and apply a review * apply reviews and add testcases * add benchmark * cleanup * rename middleware * fix linter * Update docs and config values * Revert change to IsReady * Updates based on code review * Update docs to match other middlewares --------- Co-authored-by: Muhammed Efe Cetin <efectn@protonmail.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com> * prepare release v2.52.0 - add more Parser tests * fix healthcheck.md * configure workflows for V2 branch * configure workflows for V2 branch * Fix default value to false in docs of QueryBool (#2811) fix default value to false in docs of QueryBool * update queryParser config * Update ctx.md * Update routing.md * merge v2 in v3 * merge v2 in v3 * lint fixes * 📚 Doc: Fix code snippet indentation in /docs/api/middleware/keyauth.md Removes an an extra level of indentation in line 51 of `keyauth.md` [here](https://github.com/gofiber/fiber/blob/v2/docs/api/middleware/keyauth.md?plain=1#L51) * fix: healthcheck middleware not working with route group (#2863) * fix: healthcheck middleware not working with route group * perf: change verification method to improve perf * Update healthcheck_test.go * test: add not matching route test for strict routing * add more test cases * correct tests * correct test helpers * correct tests * correct tests --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: René Werner <rene@gofiber.io> * merge v2 in v3 * Merge pull request from GHSA-fmg4-x8pw-hjhg * Enforce Wildcard Origins with AllowCredentials check * Expand unit-tests, fix issues with subdomains logic, update docs * Update cors.md * Added test using localhost, ipv4, and ipv6 address * improve documentation markdown --------- Co-authored-by: René Werner <rene@gofiber.io> * Update app.go prepare release v2.52.1 * fix cors domain normalize * fix sync-docs workflow * test: fix failing tests * fix sync-docs workflow * test: cors middleware use testify require * chore: fix lint warnings * chore: revert test isolation. * fixed the fasthttp ctx race condition problem * Update middleware/cors/utils.go Co-authored-by: Renan Bastos <renanbastos.tec@gmail.com> * fix sync_docs.sh * fix review comments/hints * fix review comments/hints * stabilize Test_Proxy_Timeout_Slow_Server test * stabilize Test_Proxy_.* tests * ignore bodyclose linter for tests use http.NoBody instead of nil * revert(tests): undo http.NoBody usage * fix(ctx pool): postpone the reset for some values shortly before the release in the pool * refactor(tests): use testify panic method instead of custom solution --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: tokelo-12 <113810058+tokelo-12@users.noreply.github.com> Co-authored-by: Jason McNeil <sixcolors@mac.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: iRedMail <2048991+iredmail@users.noreply.github.com> Co-authored-by: Benjamin Grosse <ste3ls@gmail.com> Co-authored-by: Mehmet Firat KOMURCU <mehmetfiratkomurcu@hotmail.com> Co-authored-by: Bruno <bdm2943@icloud.com> Co-authored-by: Muhammad Kholid B <muhammadkholidb@gmail.com> Co-authored-by: gilwo <gilwo@users.noreply.github.com> Co-authored-by: Lucas Lemos <lucashenriqueblemos@gmail.com> Co-authored-by: Muhammed Efe Cetin <efectn@protonmail.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com> Co-authored-by: Jongmin Kim <kjongmin26@gmail.com> Co-authored-by: Giovanni Rivera <rivera.giovanni271@gmail.com> Co-authored-by: Renan Bastos <renanbastos.tec@gmail.com>
591 lines
16 KiB
Go
591 lines
16 KiB
Go
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
|
|
// 🤖 Github Repository: https://github.com/gofiber/fiber
|
|
// 📌 API Documentation: https://docs.gofiber.io
|
|
|
|
package fiber
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gofiber/utils/v2"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// Router defines all router handle interface, including app and group router.
|
|
type Router interface {
|
|
Use(args ...any) Router
|
|
|
|
Get(path string, handler Handler, middleware ...Handler) Router
|
|
Head(path string, handler Handler, middleware ...Handler) Router
|
|
Post(path string, handler Handler, middleware ...Handler) Router
|
|
Put(path string, handler Handler, middleware ...Handler) Router
|
|
Delete(path string, handler Handler, middleware ...Handler) Router
|
|
Connect(path string, handler Handler, middleware ...Handler) Router
|
|
Options(path string, handler Handler, middleware ...Handler) Router
|
|
Trace(path string, handler Handler, middleware ...Handler) Router
|
|
Patch(path string, handler Handler, middleware ...Handler) Router
|
|
|
|
Add(methods []string, path string, handler Handler, middleware ...Handler) Router
|
|
Static(prefix, root string, config ...Static) Router
|
|
All(path string, handler Handler, middleware ...Handler) Router
|
|
|
|
Group(prefix string, handlers ...Handler) Router
|
|
|
|
Route(path string) Register
|
|
|
|
Name(name string) Router
|
|
}
|
|
|
|
// Route is a struct that holds all metadata for each registered handler.
|
|
type Route struct {
|
|
// ### important: always keep in sync with the copy method "app.copyRoute" ###
|
|
// Data for routing
|
|
pos uint32 // Position in stack -> important for the sort of the matched routes
|
|
use bool // USE matches path prefixes
|
|
mount bool // Indicated a mounted app on a specific route
|
|
star bool // Path equals '*'
|
|
root bool // Path equals '/'
|
|
path string // Prettified path
|
|
routeParser routeParser // Parameter parser
|
|
group *Group // Group instance. used for routes in groups
|
|
|
|
// Public fields
|
|
Method string `json:"method"` // HTTP method
|
|
Name string `json:"name"` // Route's name
|
|
//nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine
|
|
Path string `json:"path"` // Original registered route path
|
|
Params []string `json:"params"` // Case sensitive param keys
|
|
Handlers []Handler `json:"-"` // Ctx handlers
|
|
}
|
|
|
|
func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
|
|
// root detectionPath check
|
|
if r.root && detectionPath == "/" {
|
|
return true
|
|
// '*' wildcard matches any detectionPath
|
|
} else if r.star {
|
|
if len(path) > 1 {
|
|
params[0] = path[1:]
|
|
} else {
|
|
params[0] = ""
|
|
}
|
|
return true
|
|
}
|
|
// Does this route have parameters
|
|
if len(r.Params) > 0 {
|
|
// Match params
|
|
if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
|
|
// Get params from the path detectionPath
|
|
return match
|
|
}
|
|
}
|
|
// Is this route a Middleware?
|
|
if r.use {
|
|
// Single slash will match or detectionPath prefix
|
|
if r.root || strings.HasPrefix(detectionPath, r.path) {
|
|
return true
|
|
}
|
|
// Check for a simple detectionPath match
|
|
} else if len(r.path) == len(detectionPath) && r.path == detectionPath {
|
|
return true
|
|
}
|
|
// No match
|
|
return false
|
|
}
|
|
|
|
func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint: unparam // bool param might be useful for testing
|
|
// Get stack length
|
|
tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()]
|
|
if !ok {
|
|
tree = app.treeStack[c.getMethodINT()][""]
|
|
}
|
|
lenr := len(tree) - 1
|
|
|
|
// Loop over the route stack starting from previous index
|
|
for c.getIndexRoute() < lenr {
|
|
// Increment route index
|
|
c.setIndexRoute(c.getIndexRoute() + 1)
|
|
|
|
// Get *Route
|
|
route := tree[c.getIndexRoute()]
|
|
|
|
// Check if it matches the request path
|
|
match := route.match(c.getDetectionPath(), c.Path(), c.getValues())
|
|
|
|
// No match, next route
|
|
if !match {
|
|
continue
|
|
}
|
|
// Pass route reference and param values
|
|
c.setRoute(route)
|
|
|
|
// Non use handler matched
|
|
if !c.getMatched() && !route.use {
|
|
c.setMatched(true)
|
|
}
|
|
|
|
// Execute first handler of route
|
|
c.setIndexHandler(0)
|
|
err := route.Handlers[0](c)
|
|
return match, err // Stop scanning the stack
|
|
}
|
|
|
|
// If c.Next() does not match, return 404
|
|
err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+c.getPathOriginal())
|
|
|
|
// If no match, scan stack again if other methods match the request
|
|
// Moved from app.handler because middleware may break the route chain
|
|
if !c.getMatched() && app.methodExistCustom(c) {
|
|
err = ErrMethodNotAllowed
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (app *App) next(c *DefaultCtx) (bool, error) {
|
|
// Get stack length
|
|
tree, ok := app.treeStack[c.methodINT][c.treePath]
|
|
if !ok {
|
|
tree = app.treeStack[c.methodINT][""]
|
|
}
|
|
lenTree := len(tree) - 1
|
|
|
|
// Loop over the route stack starting from previous index
|
|
for c.indexRoute < lenTree {
|
|
// Increment route index
|
|
c.indexRoute++
|
|
|
|
// Get *Route
|
|
route := tree[c.indexRoute]
|
|
|
|
var match bool
|
|
var err error
|
|
// skip for mounted apps
|
|
if route.mount {
|
|
continue
|
|
}
|
|
|
|
// Check if it matches the request path
|
|
match = route.match(c.detectionPath, c.path, &c.values)
|
|
if !match {
|
|
// No match, next route
|
|
continue
|
|
}
|
|
// Pass route reference and param values
|
|
c.route = route
|
|
|
|
// Non use handler matched
|
|
if !c.matched && !route.use {
|
|
c.matched = true
|
|
}
|
|
|
|
// Execute first handler of route
|
|
c.indexHandler = 0
|
|
if len(route.Handlers) > 0 {
|
|
err = route.Handlers[0](c)
|
|
}
|
|
return match, err // Stop scanning the stack
|
|
}
|
|
|
|
// If c.Next() does not match, return 404
|
|
err := NewError(StatusNotFound, "Cannot "+c.method+" "+html.EscapeString(c.pathOriginal))
|
|
if !c.matched && app.methodExist(c) {
|
|
// If no match, scan stack again if other methods match the request
|
|
// Moved from app.handler because middleware may break the route chain
|
|
err = ErrMethodNotAllowed
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (app *App) requestHandler(rctx *fasthttp.RequestCtx) {
|
|
// Handler for default ctxs
|
|
var c CustomCtx
|
|
var ok bool
|
|
if app.newCtxFunc != nil {
|
|
c, ok = app.AcquireCtx(rctx).(CustomCtx)
|
|
if !ok {
|
|
panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
|
|
}
|
|
} else {
|
|
c, ok = app.AcquireCtx(rctx).(*DefaultCtx)
|
|
if !ok {
|
|
panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
|
|
}
|
|
}
|
|
c.Reset(rctx)
|
|
defer app.ReleaseCtx(c)
|
|
|
|
// handle invalid http method directly
|
|
if app.methodInt(c.Method()) == -1 {
|
|
_ = c.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
|
|
return
|
|
}
|
|
|
|
// check flash messages
|
|
if strings.Contains(utils.UnsafeString(c.Request().Header.RawHeaders()), FlashCookieName) {
|
|
c.Redirect().setFlash()
|
|
}
|
|
|
|
// Find match in stack
|
|
var err error
|
|
if app.newCtxFunc != nil {
|
|
_, err = app.nextCustom(c)
|
|
} else {
|
|
_, err = app.next(c.(*DefaultCtx))
|
|
}
|
|
if err != nil {
|
|
if catch := c.App().ErrorHandler(c, err); catch != nil {
|
|
_ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here
|
|
}
|
|
// TODO: Do we need to return here?
|
|
}
|
|
}
|
|
|
|
func (app *App) addPrefixToRoute(prefix string, route *Route) *Route {
|
|
prefixedPath := getGroupPath(prefix, route.Path)
|
|
prettyPath := prefixedPath
|
|
// Case-sensitive routing, all to lowercase
|
|
if !app.config.CaseSensitive {
|
|
prettyPath = utils.ToLower(prettyPath)
|
|
}
|
|
// Strict routing, remove trailing slashes
|
|
if !app.config.StrictRouting && len(prettyPath) > 1 {
|
|
prettyPath = strings.TrimRight(prettyPath, "/")
|
|
}
|
|
|
|
route.Path = prefixedPath
|
|
route.path = RemoveEscapeChar(prettyPath)
|
|
route.routeParser = parseRoute(prettyPath, app.customConstraints...)
|
|
route.root = false
|
|
route.star = false
|
|
|
|
return route
|
|
}
|
|
|
|
func (*App) copyRoute(route *Route) *Route {
|
|
return &Route{
|
|
// Router booleans
|
|
use: route.use,
|
|
mount: route.mount,
|
|
star: route.star,
|
|
root: route.root,
|
|
|
|
// Path data
|
|
path: route.path,
|
|
routeParser: route.routeParser,
|
|
|
|
// misc
|
|
pos: route.pos,
|
|
|
|
// Public data
|
|
Path: route.Path,
|
|
Params: route.Params,
|
|
Name: route.Name,
|
|
Method: route.Method,
|
|
Handlers: route.Handlers,
|
|
}
|
|
}
|
|
|
|
func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) {
|
|
handlers := middleware
|
|
if handler != nil {
|
|
handlers = append(handlers, handler)
|
|
}
|
|
|
|
for _, method := range methods {
|
|
// Uppercase HTTP methods
|
|
method = utils.ToUpper(method)
|
|
// Check if the HTTP method is valid unless it's USE
|
|
if method != methodUse && app.methodInt(method) == -1 {
|
|
panic(fmt.Sprintf("add: invalid http method %s\n", method))
|
|
}
|
|
// is mounted app
|
|
isMount := group != nil && group.app != app
|
|
// A route requires atleast one ctx handler
|
|
if len(handlers) == 0 && !isMount {
|
|
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
|
|
}
|
|
// Cannot have an empty path
|
|
if pathRaw == "" {
|
|
pathRaw = "/"
|
|
}
|
|
// Path always start with a '/'
|
|
if pathRaw[0] != '/' {
|
|
pathRaw = "/" + pathRaw
|
|
}
|
|
// Create a stripped path in-case sensitive / trailing slashes
|
|
pathPretty := pathRaw
|
|
// Case-sensitive routing, all to lowercase
|
|
if !app.config.CaseSensitive {
|
|
pathPretty = utils.ToLower(pathPretty)
|
|
}
|
|
// Strict routing, remove trailing slashes
|
|
if !app.config.StrictRouting && len(pathPretty) > 1 {
|
|
pathPretty = strings.TrimRight(pathPretty, "/")
|
|
}
|
|
// Is layer a middleware?
|
|
isUse := method == methodUse
|
|
// Is path a direct wildcard?
|
|
isStar := pathPretty == "/*"
|
|
// Is path a root slash?
|
|
isRoot := pathPretty == "/"
|
|
// Parse path parameters
|
|
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
|
|
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
|
|
|
|
// Create route metadata without pointer
|
|
route := Route{
|
|
// Router booleans
|
|
use: isUse,
|
|
mount: isMount,
|
|
star: isStar,
|
|
root: isRoot,
|
|
|
|
// Path data
|
|
path: RemoveEscapeChar(pathPretty),
|
|
routeParser: parsedPretty,
|
|
Params: parsedRaw.params,
|
|
|
|
// Group data
|
|
group: group,
|
|
|
|
// Public data
|
|
Path: pathRaw,
|
|
Method: method,
|
|
Handlers: handlers,
|
|
}
|
|
// Increment global handler count
|
|
atomic.AddUint32(&app.handlersCount, uint32(len(handlers)))
|
|
|
|
// Middleware route matches all HTTP methods
|
|
if isUse {
|
|
// Add route to all HTTP methods stack
|
|
for _, m := range app.config.RequestMethods {
|
|
// Create a route copy to avoid duplicates during compression
|
|
r := route
|
|
app.addRoute(m, &r, isMount)
|
|
}
|
|
} else {
|
|
// Add route to stack
|
|
app.addRoute(method, &route, isMount)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (app *App) registerStatic(prefix, root string, config ...Static) {
|
|
// For security, we want to restrict to the current work directory.
|
|
if root == "" {
|
|
root = "."
|
|
}
|
|
// Cannot have an empty prefix
|
|
if prefix == "" {
|
|
prefix = "/"
|
|
}
|
|
// Prefix always start with a '/' or '*'
|
|
if prefix[0] != '/' {
|
|
prefix = "/" + prefix
|
|
}
|
|
// in case-sensitive routing, all to lowercase
|
|
if !app.config.CaseSensitive {
|
|
prefix = utils.ToLower(prefix)
|
|
}
|
|
// Strip trailing slashes from the root path
|
|
if len(root) > 0 && root[len(root)-1] == '/' {
|
|
root = root[:len(root)-1]
|
|
}
|
|
// Is prefix a direct wildcard?
|
|
isStar := prefix == "/*"
|
|
// Is prefix a root slash?
|
|
isRoot := prefix == "/"
|
|
// Is prefix a partial wildcard?
|
|
if strings.Contains(prefix, "*") {
|
|
// /john* -> /john
|
|
isStar = true
|
|
prefix = strings.Split(prefix, "*")[0]
|
|
// Fix this later
|
|
}
|
|
prefixLen := len(prefix)
|
|
if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
|
|
// /john/ -> /john
|
|
prefixLen--
|
|
prefix = prefix[:prefixLen]
|
|
}
|
|
const cacheDuration = 10 * time.Second
|
|
// Fileserver settings
|
|
fs := &fasthttp.FS{
|
|
Root: root,
|
|
AllowEmptyRoot: true,
|
|
GenerateIndexPages: false,
|
|
AcceptByteRange: false,
|
|
Compress: false,
|
|
CompressedFileSuffix: app.config.CompressedFileSuffix,
|
|
CacheDuration: cacheDuration,
|
|
IndexNames: []string{"index.html"},
|
|
PathRewrite: func(fctx *fasthttp.RequestCtx) []byte {
|
|
path := fctx.Path()
|
|
if len(path) >= prefixLen {
|
|
if isStar && app.getString(path[0:prefixLen]) == prefix {
|
|
path = append(path[0:0], '/')
|
|
} else {
|
|
path = path[prefixLen:]
|
|
if len(path) == 0 || path[len(path)-1] != '/' {
|
|
path = append(path, '/')
|
|
}
|
|
}
|
|
}
|
|
if len(path) > 0 && path[0] != '/' {
|
|
path = append([]byte("/"), path...)
|
|
}
|
|
return path
|
|
},
|
|
PathNotFound: func(fctx *fasthttp.RequestCtx) {
|
|
fctx.Response.SetStatusCode(StatusNotFound)
|
|
},
|
|
}
|
|
|
|
// Set config if provided
|
|
var cacheControlValue string
|
|
var modifyResponse Handler
|
|
if len(config) > 0 {
|
|
maxAge := config[0].MaxAge
|
|
if maxAge > 0 {
|
|
cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
|
|
}
|
|
fs.CacheDuration = config[0].CacheDuration
|
|
fs.Compress = config[0].Compress
|
|
fs.AcceptByteRange = config[0].ByteRange
|
|
fs.GenerateIndexPages = config[0].Browse
|
|
if config[0].Index != "" {
|
|
fs.IndexNames = []string{config[0].Index}
|
|
}
|
|
modifyResponse = config[0].ModifyResponse
|
|
}
|
|
fileHandler := fs.NewRequestHandler()
|
|
handler := func(c Ctx) error {
|
|
// Don't execute middleware if Next returns true
|
|
if len(config) != 0 && config[0].Next != nil && config[0].Next(c) {
|
|
return c.Next()
|
|
}
|
|
// Serve file
|
|
fileHandler(c.Context())
|
|
// Sets the response Content-Disposition header to attachment if the Download option is true
|
|
if len(config) > 0 && config[0].Download {
|
|
c.Attachment()
|
|
}
|
|
// Return request if found and not forbidden
|
|
status := c.Context().Response.StatusCode()
|
|
if status != StatusNotFound && status != StatusForbidden {
|
|
if len(cacheControlValue) > 0 {
|
|
c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue)
|
|
}
|
|
if modifyResponse != nil {
|
|
return modifyResponse(c)
|
|
}
|
|
return nil
|
|
}
|
|
// Reset response to default
|
|
c.Context().SetContentType("") // Issue #420
|
|
c.Context().Response.SetStatusCode(StatusOK)
|
|
c.Context().Response.SetBodyString("")
|
|
// Next middleware
|
|
return c.Next()
|
|
}
|
|
|
|
// Create route metadata without pointer
|
|
route := Route{
|
|
// Router booleans
|
|
use: true,
|
|
root: isRoot,
|
|
path: prefix,
|
|
// Public data
|
|
Method: MethodGet,
|
|
Path: prefix,
|
|
Handlers: []Handler{handler},
|
|
}
|
|
// Increment global handler count
|
|
atomic.AddUint32(&app.handlersCount, 1)
|
|
// Add route to stack
|
|
app.addRoute(MethodGet, &route)
|
|
// Add HEAD route
|
|
app.addRoute(MethodHead, &route)
|
|
}
|
|
|
|
func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
|
|
// Check mounted routes
|
|
var mounted bool
|
|
if len(isMounted) > 0 {
|
|
mounted = isMounted[0]
|
|
}
|
|
|
|
// Get unique HTTP method identifier
|
|
m := app.methodInt(method)
|
|
|
|
// prevent identically route registration
|
|
l := len(app.stack[m])
|
|
if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount {
|
|
preRoute := app.stack[m][l-1]
|
|
preRoute.Handlers = append(preRoute.Handlers, route.Handlers...)
|
|
} else {
|
|
// Increment global route position
|
|
route.pos = atomic.AddUint32(&app.routesCount, 1)
|
|
route.Method = method
|
|
// Add route to the stack
|
|
app.stack[m] = append(app.stack[m], route)
|
|
app.routesRefreshed = true
|
|
}
|
|
|
|
// Execute onRoute hooks & change latestRoute if not adding mounted route
|
|
if !mounted {
|
|
app.mutex.Lock()
|
|
app.latestRoute = route
|
|
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
|
|
panic(err)
|
|
}
|
|
app.mutex.Unlock()
|
|
}
|
|
}
|
|
|
|
// buildTree build the prefix tree from the previously registered routes
|
|
func (app *App) buildTree() *App {
|
|
if !app.routesRefreshed {
|
|
return app
|
|
}
|
|
|
|
// loop all the methods and stacks and create the prefix tree
|
|
for m := range app.config.RequestMethods {
|
|
tsMap := make(map[string][]*Route)
|
|
for _, route := range app.stack[m] {
|
|
treePath := ""
|
|
if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 {
|
|
treePath = route.routeParser.segs[0].Const[:3]
|
|
}
|
|
// create tree stack
|
|
tsMap[treePath] = append(tsMap[treePath], route)
|
|
}
|
|
app.treeStack[m] = tsMap
|
|
}
|
|
|
|
// loop the methods and tree stacks and add global stack and sort everything
|
|
for m := range app.config.RequestMethods {
|
|
tsMap := app.treeStack[m]
|
|
for treePart := range tsMap {
|
|
if treePart != "" {
|
|
// merge global tree routes in current tree stack
|
|
tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...))
|
|
}
|
|
// sort tree slices with the positions
|
|
slc := tsMap[treePart]
|
|
sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos })
|
|
}
|
|
}
|
|
app.routesRefreshed = false
|
|
|
|
return app
|
|
}
|