router.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. // ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
  2. // 🤖 Github Repository: https://github.com/gofiber/fiber
  3. // 📌 API Documentation: https://docs.gofiber.io
  4. package fiber
  5. import (
  6. "fmt"
  7. "html"
  8. "strconv"
  9. "strings"
  10. "sync/atomic"
  11. "time"
  12. "github.com/gofiber/fiber/v2/utils"
  13. "github.com/valyala/fasthttp"
  14. )
  15. // Router defines all router handle interface, including app and group router.
  16. type Router interface {
  17. Use(args ...interface{}) Router
  18. Get(path string, handlers ...Handler) Router
  19. Head(path string, handlers ...Handler) Router
  20. Post(path string, handlers ...Handler) Router
  21. Put(path string, handlers ...Handler) Router
  22. Delete(path string, handlers ...Handler) Router
  23. Connect(path string, handlers ...Handler) Router
  24. Options(path string, handlers ...Handler) Router
  25. Trace(path string, handlers ...Handler) Router
  26. Patch(path string, handlers ...Handler) Router
  27. Add(method, path string, handlers ...Handler) Router
  28. Static(prefix, root string, config ...Static) Router
  29. All(path string, handlers ...Handler) Router
  30. Group(prefix string, handlers ...Handler) Router
  31. Route(prefix string, fn func(router Router), name ...string) Router
  32. Mount(prefix string, fiber *App) Router
  33. Name(name string) Router
  34. }
  35. // Route is a struct that holds all metadata for each registered handler.
  36. type Route struct {
  37. // ### important: always keep in sync with the copy method "app.copyRoute" and all creations of Route struct ###
  38. // Data for routing
  39. use bool // USE matches path prefixes
  40. mount bool // Indicated a mounted app on a specific route
  41. star bool // Path equals '*'
  42. root bool // Path equals '/'
  43. path string // Prettified path
  44. routeParser routeParser // Parameter parser
  45. group *Group // Group instance. used for routes in groups
  46. // Public fields
  47. Method string `json:"method"` // HTTP method
  48. Name string `json:"name"` // Route's name
  49. //nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine
  50. Path string `json:"path"` // Original registered route path
  51. Params []string `json:"params"` // Case sensitive param keys
  52. Handlers []Handler `json:"-"` // Ctx handlers
  53. }
  54. func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
  55. // root detectionPath check
  56. if r.root && detectionPath == "/" {
  57. return true
  58. // '*' wildcard matches any detectionPath
  59. } else if r.star {
  60. if len(path) > 1 {
  61. params[0] = path[1:]
  62. } else {
  63. params[0] = ""
  64. }
  65. return true
  66. }
  67. // Does this route have parameters
  68. if len(r.Params) > 0 {
  69. // Match params
  70. if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
  71. // Get params from the path detectionPath
  72. return match
  73. }
  74. }
  75. // Is this route a Middleware?
  76. if r.use {
  77. // Single slash will match or detectionPath prefix
  78. if r.root || strings.HasPrefix(detectionPath, r.path) {
  79. return true
  80. }
  81. // Check for a simple detectionPath match
  82. } else if len(r.path) == len(detectionPath) && r.path == detectionPath {
  83. return true
  84. }
  85. // No match
  86. return false
  87. }
  88. func (app *App) next(c *Ctx) (bool, error) {
  89. // Get stack length
  90. tree, ok := app.treeStack[c.methodINT][c.treePath]
  91. if !ok {
  92. tree = app.treeStack[c.methodINT][""]
  93. }
  94. lenTree := len(tree) - 1
  95. // Loop over the route stack starting from previous index
  96. for c.indexRoute < lenTree {
  97. // Increment route index
  98. c.indexRoute++
  99. // Get *Route
  100. route := tree[c.indexRoute]
  101. var match bool
  102. var err error
  103. // skip for mounted apps
  104. if route.mount {
  105. continue
  106. }
  107. // Check if it matches the request path
  108. match = route.match(c.detectionPath, c.path, &c.values)
  109. if !match {
  110. // No match, next route
  111. continue
  112. }
  113. // Pass route reference and param values
  114. c.route = route
  115. // Non use handler matched
  116. if !c.matched && !route.use {
  117. c.matched = true
  118. }
  119. // Execute first handler of route
  120. c.indexHandler = 0
  121. if len(route.Handlers) > 0 {
  122. err = route.Handlers[0](c)
  123. }
  124. return match, err // Stop scanning the stack
  125. }
  126. // If c.Next() does not match, return 404
  127. err := NewError(StatusNotFound, "Cannot "+c.method+" "+html.EscapeString(c.pathOriginal))
  128. if !c.matched && app.methodExist(c) {
  129. // If no match, scan stack again if other methods match the request
  130. // Moved from app.handler because middleware may break the route chain
  131. err = ErrMethodNotAllowed
  132. }
  133. return false, err
  134. }
  135. func (app *App) handler(rctx *fasthttp.RequestCtx) { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476
  136. // Acquire Ctx with fasthttp request from pool
  137. c := app.AcquireCtx(rctx)
  138. defer app.ReleaseCtx(c)
  139. // handle invalid http method directly
  140. if c.methodINT == -1 {
  141. _ = c.Status(StatusBadRequest).SendString("Invalid http method") //nolint:errcheck // It is fine to ignore the error here
  142. return
  143. }
  144. // Find match in stack
  145. match, err := app.next(c)
  146. if err != nil {
  147. if catch := c.app.ErrorHandler(c, err); catch != nil {
  148. _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here
  149. }
  150. // TODO: Do we need to return here?
  151. }
  152. // Generate ETag if enabled
  153. if match && app.config.ETag {
  154. setETag(c, false)
  155. }
  156. }
  157. func (app *App) addPrefixToRoute(prefix string, route *Route) *Route {
  158. prefixedPath := getGroupPath(prefix, route.Path)
  159. prettyPath := prefixedPath
  160. // Case-sensitive routing, all to lowercase
  161. if !app.config.CaseSensitive {
  162. prettyPath = utils.ToLower(prettyPath)
  163. }
  164. // Strict routing, remove trailing slashes
  165. if !app.config.StrictRouting && len(prettyPath) > 1 {
  166. prettyPath = utils.TrimRight(prettyPath, '/')
  167. }
  168. route.Path = prefixedPath
  169. route.path = RemoveEscapeChar(prettyPath)
  170. route.routeParser = parseRoute(prettyPath)
  171. route.root = false
  172. route.star = false
  173. return route
  174. }
  175. func (*App) copyRoute(route *Route) *Route {
  176. return &Route{
  177. // Router booleans
  178. use: route.use,
  179. mount: route.mount,
  180. star: route.star,
  181. root: route.root,
  182. // Path data
  183. path: route.path,
  184. routeParser: route.routeParser,
  185. // Public data
  186. Path: route.Path,
  187. Params: route.Params,
  188. Name: route.Name,
  189. Method: route.Method,
  190. Handlers: route.Handlers,
  191. }
  192. }
  193. func (app *App) register(method, pathRaw string, group *Group, handlers ...Handler) {
  194. // Uppercase HTTP methods
  195. method = utils.ToUpper(method)
  196. // Check if the HTTP method is valid unless it's USE
  197. if method != methodUse && app.methodInt(method) == -1 {
  198. panic(fmt.Sprintf("add: invalid http method %s\n", method))
  199. }
  200. // is mounted app
  201. isMount := group != nil && group.app != app
  202. // A route requires atleast one ctx handler
  203. if len(handlers) == 0 && !isMount {
  204. panic(fmt.Sprintf("missing handler in route: %s\n", pathRaw))
  205. }
  206. // Cannot have an empty path
  207. if pathRaw == "" {
  208. pathRaw = "/"
  209. }
  210. // Path always start with a '/'
  211. if pathRaw[0] != '/' {
  212. pathRaw = "/" + pathRaw
  213. }
  214. // Create a stripped path in-case sensitive / trailing slashes
  215. pathPretty := pathRaw
  216. // Case-sensitive routing, all to lowercase
  217. if !app.config.CaseSensitive {
  218. pathPretty = utils.ToLower(pathPretty)
  219. }
  220. // Strict routing, remove trailing slashes
  221. if !app.config.StrictRouting && len(pathPretty) > 1 {
  222. pathPretty = utils.TrimRight(pathPretty, '/')
  223. }
  224. // Is layer a middleware?
  225. isUse := method == methodUse
  226. // Is path a direct wildcard?
  227. isStar := pathPretty == "/*"
  228. // Is path a root slash?
  229. isRoot := pathPretty == "/"
  230. // Parse path parameters
  231. parsedRaw := parseRoute(pathRaw)
  232. parsedPretty := parseRoute(pathPretty)
  233. // Create route metadata without pointer
  234. route := Route{
  235. // Router booleans
  236. use: isUse,
  237. mount: isMount,
  238. star: isStar,
  239. root: isRoot,
  240. // Path data
  241. path: RemoveEscapeChar(pathPretty),
  242. routeParser: parsedPretty,
  243. Params: parsedRaw.params,
  244. // Group data
  245. group: group,
  246. // Public data
  247. Path: pathRaw,
  248. Method: method,
  249. Handlers: handlers,
  250. }
  251. // Increment global handler count
  252. atomic.AddUint32(&app.handlersCount, uint32(len(handlers)))
  253. // Middleware route matches all HTTP methods
  254. if isUse {
  255. // Add route to all HTTP methods stack
  256. for _, m := range app.config.RequestMethods {
  257. // Create a route copy to avoid duplicates during compression
  258. r := route
  259. app.addRoute(m, &r)
  260. }
  261. } else {
  262. // Add route to stack
  263. app.addRoute(method, &route)
  264. }
  265. }
  266. func (app *App) registerStatic(prefix, root string, config ...Static) {
  267. // For security, we want to restrict to the current work directory.
  268. if root == "" {
  269. root = "."
  270. }
  271. // Cannot have an empty prefix
  272. if prefix == "" {
  273. prefix = "/"
  274. }
  275. // Prefix always start with a '/' or '*'
  276. if prefix[0] != '/' {
  277. prefix = "/" + prefix
  278. }
  279. // in case-sensitive routing, all to lowercase
  280. if !app.config.CaseSensitive {
  281. prefix = utils.ToLower(prefix)
  282. }
  283. // Strip trailing slashes from the root path
  284. if len(root) > 0 && root[len(root)-1] == '/' {
  285. root = root[:len(root)-1]
  286. }
  287. // Is prefix a direct wildcard?
  288. isStar := prefix == "/*"
  289. // Is prefix a root slash?
  290. isRoot := prefix == "/"
  291. // Is prefix a partial wildcard?
  292. if strings.Contains(prefix, "*") {
  293. // /john* -> /john
  294. isStar = true
  295. prefix = strings.Split(prefix, "*")[0]
  296. // Fix this later
  297. }
  298. prefixLen := len(prefix)
  299. if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
  300. // /john/ -> /john
  301. prefixLen--
  302. prefix = prefix[:prefixLen]
  303. }
  304. const cacheDuration = 10 * time.Second
  305. // Fileserver settings
  306. fs := &fasthttp.FS{
  307. Root: root,
  308. AllowEmptyRoot: true,
  309. GenerateIndexPages: false,
  310. AcceptByteRange: false,
  311. Compress: false,
  312. CompressedFileSuffix: app.config.CompressedFileSuffix,
  313. CacheDuration: cacheDuration,
  314. IndexNames: []string{"index.html"},
  315. PathRewrite: func(fctx *fasthttp.RequestCtx) []byte {
  316. path := fctx.Path()
  317. if len(path) >= prefixLen {
  318. if isStar && app.getString(path[0:prefixLen]) == prefix {
  319. path = append(path[0:0], '/')
  320. } else {
  321. path = path[prefixLen:]
  322. if len(path) == 0 || path[len(path)-1] != '/' {
  323. path = append(path, '/')
  324. }
  325. }
  326. }
  327. if len(path) > 0 && path[0] != '/' {
  328. path = append([]byte("/"), path...)
  329. }
  330. return path
  331. },
  332. PathNotFound: func(fctx *fasthttp.RequestCtx) {
  333. fctx.Response.SetStatusCode(StatusNotFound)
  334. },
  335. }
  336. // Set config if provided
  337. var cacheControlValue string
  338. var modifyResponse Handler
  339. if len(config) > 0 {
  340. maxAge := config[0].MaxAge
  341. if maxAge > 0 {
  342. cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
  343. }
  344. fs.CacheDuration = config[0].CacheDuration
  345. fs.Compress = config[0].Compress
  346. fs.AcceptByteRange = config[0].ByteRange
  347. fs.GenerateIndexPages = config[0].Browse
  348. if config[0].Index != "" {
  349. fs.IndexNames = []string{config[0].Index}
  350. }
  351. modifyResponse = config[0].ModifyResponse
  352. }
  353. fileHandler := fs.NewRequestHandler()
  354. handler := func(c *Ctx) error {
  355. // Don't execute middleware if Next returns true
  356. if len(config) != 0 && config[0].Next != nil && config[0].Next(c) {
  357. return c.Next()
  358. }
  359. // Serve file
  360. fileHandler(c.fasthttp)
  361. // Sets the response Content-Disposition header to attachment if the Download option is true
  362. if len(config) > 0 && config[0].Download {
  363. c.Attachment()
  364. }
  365. // Return request if found and not forbidden
  366. status := c.fasthttp.Response.StatusCode()
  367. if status != StatusNotFound && status != StatusForbidden {
  368. if len(cacheControlValue) > 0 {
  369. c.fasthttp.Response.Header.Set(HeaderCacheControl, cacheControlValue)
  370. }
  371. if modifyResponse != nil {
  372. return modifyResponse(c)
  373. }
  374. return nil
  375. }
  376. // Reset response to default
  377. c.fasthttp.SetContentType("") // Issue #420
  378. c.fasthttp.Response.SetStatusCode(StatusOK)
  379. c.fasthttp.Response.SetBodyString("")
  380. // Next middleware
  381. return c.Next()
  382. }
  383. // Create route metadata without pointer
  384. route := Route{
  385. // Router booleans
  386. use: true,
  387. mount: false,
  388. star: isStar,
  389. root: isRoot,
  390. // Path data
  391. path: prefix,
  392. // Group data
  393. group: nil,
  394. // Public data
  395. Path: prefix,
  396. Method: MethodGet,
  397. Handlers: []Handler{handler},
  398. }
  399. // Increment global handler count
  400. atomic.AddUint32(&app.handlersCount, 1)
  401. // Add route to stack
  402. app.addRoute(MethodGet, &route)
  403. // Add HEAD route
  404. app.addRoute(MethodHead, &route)
  405. }
  406. func (app *App) addRoute(method string, route *Route) {
  407. // Get unique HTTP method identifier
  408. m := app.methodInt(method)
  409. // prevent identically route registration
  410. l := len(app.stack[m])
  411. 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 {
  412. preRoute := app.stack[m][l-1]
  413. preRoute.Handlers = append(preRoute.Handlers, route.Handlers...)
  414. } else {
  415. route.Method = method
  416. // Add route to the stack
  417. app.stack[m] = append(app.stack[m], route)
  418. app.routesRefreshed = true
  419. }
  420. // Execute onRoute hooks & change latestRoute if not adding mounted route
  421. if !route.mount {
  422. app.mutex.Lock()
  423. app.latestRoute = route
  424. if err := app.hooks.executeOnRouteHooks(*route); err != nil {
  425. panic(err)
  426. }
  427. app.mutex.Unlock()
  428. }
  429. }
  430. // buildTree build the prefix tree from the previously registered routes
  431. func (app *App) buildTree() *App {
  432. // If routes haven't been refreshed, nothing to do
  433. if !app.routesRefreshed {
  434. return app
  435. }
  436. // 1) First loop: determine all possible 3-char prefixes ("treePaths") for each method
  437. for method := range app.config.RequestMethods {
  438. prefixSet := map[string]struct{}{
  439. "": {},
  440. }
  441. for _, route := range app.stack[method] {
  442. if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 {
  443. prefix := route.routeParser.segs[0].Const[:3]
  444. prefixSet[prefix] = struct{}{}
  445. }
  446. }
  447. tsMap := make(map[string][]*Route, len(prefixSet))
  448. for prefix := range prefixSet {
  449. tsMap[prefix] = nil
  450. }
  451. app.treeStack[method] = tsMap
  452. }
  453. // 2) Second loop: for each method and each discovered treePath, assign matching routes
  454. for method := range app.config.RequestMethods {
  455. // get the map of buckets for this method
  456. tsMap := app.treeStack[method]
  457. // for every treePath key (including the empty one)
  458. for treePath := range tsMap {
  459. // iterate all routes of this method
  460. for _, route := range app.stack[method] {
  461. // compute this route's own prefix ("" or first 3 chars)
  462. routePath := ""
  463. if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 {
  464. routePath = route.routeParser.segs[0].Const[:3]
  465. }
  466. // if it's a global route, assign to every bucket
  467. if routePath == "" {
  468. tsMap[treePath] = append(tsMap[treePath], route)
  469. // otherwise only assign if this route's prefix matches the current bucket's key
  470. } else if routePath == treePath {
  471. tsMap[treePath] = append(tsMap[treePath], route)
  472. }
  473. }
  474. // after collecting, dedupe the bucket if it's not the global one
  475. tsMap[treePath] = uniqueRouteStack(tsMap[treePath])
  476. }
  477. }
  478. // reset the flag and return
  479. app.routesRefreshed = false
  480. return app
  481. }