pax_global_header00006660000000000000000000000064147353436010014520gustar00rootroot0000000000000052 comment=a90f305c450b81b40943f41904d685d243766b02 router-1.5.4/000077500000000000000000000000001473534360100130475ustar00rootroot00000000000000router-1.5.4/.github/000077500000000000000000000000001473534360100144075ustar00rootroot00000000000000router-1.5.4/.github/workflows/000077500000000000000000000000001473534360100164445ustar00rootroot00000000000000router-1.5.4/.github/workflows/test.yml000066400000000000000000000014741473534360100201540ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: test: strategy: matrix: go-version: [1.21.x, 1.22.x, 1.23.x] os: [ubuntu-latest, macos-latest, windows-latest, macos-14] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - run: go version - run: go test -v -cover -shuffle=on ./... - run: go test -v -cover -shuffle=on -race ./... - name: Send coverage uses: shogo82148/actions-goveralls@v1 with: flag-name: Go-${{ matrix.os }}-${{ matrix.go-version }} parallel: true finish: needs: test runs-on: ubuntu-latest steps: - uses: shogo82148/actions-goveralls@v1 with: parallel-finished: true router-1.5.4/.gitignore000066400000000000000000000004511473534360100150370ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out .coverprofile # Dependency directories (remove the comment below to include it) vendor/ # IDE .vscode/ router-1.5.4/LICENSE000066400000000000000000000030571473534360100140610ustar00rootroot00000000000000Copyright (c) 2013 Julien Schmidt Copyright (c) 2015-2016, 招牌疯子 Copyright (c) 2018-present Sergio Andres Virviescas Santana, fasthttp All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of uq nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. router-1.5.4/README.md000066400000000000000000000267351473534360100143430ustar00rootroot00000000000000# Router [![Test status](https://github.com/fasthttp/router/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/fasthttp/router/actions?workflow=test) [![Coverage Status](https://coveralls.io/repos/fasthttp/router/badge.svg?branch=master&service=github)](https://coveralls.io/github/fasthttp/router?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/fasthttp/router)](https://goreportcard.com/report/github.com/fasthttp/router) [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/fasthttp/router) [![GitHub release](https://img.shields.io/github/release/fasthttp/router.svg)](https://github.com/fasthttp/router/releases) Router is a lightweight high performance HTTP request router (also called _multiplexer_ or just _mux_ for short) for [fasthttp](https://github.com/valyala/fasthttp). This router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching. Based on [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter). ## Features **Best Performance:** Router is **one of the fastest** go web frameworks in the [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark). Even faster than httprouter itself. - Basic Test: The first test case is to mock 0 ms, 10 ms, 100 ms, 500 ms processing time in handlers. ![](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/master/benchmark.png) - Concurrency Test (allocations): In 30 ms processing time, the test result for 100, 1000, 5000 clients is: \* _Smaller is better_ ![](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/master/concurrency_alloc.png) See below for technical details of the implementation. **Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux), a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like _longest match_ or _first registered, first matched_. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience. **Stop caring about trailing slashes:** Choose the URL style you like, the router automatically redirects the client if a trailing slash is missing or if there is one extra. Of course it only does so, if the new path has a handler. **If** you don't like it, you can [turn off this behavior](https://pkg.go.dev/github.com/fasthttp/router#Router.RedirectTrailingSlash). **Path auto-correction:** Besides detecting the missing or additional trailing slash at no extra cost, the router can also fix wrong cases and remove superfluous path elements (like `../` or `//`). Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? Router can help him by making a case-insensitive look-up and redirecting him to the correct URL. **Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap. **Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. In fact, the only heap allocations that are made, is by building the slice of the key-value pairs for path parameters. If the request path contains no parameters, not a single heap allocation is necessary. **No more server crashes:** You can set a [Panic handler](https://pkg.go.dev/github.com/fasthttp/router#Router.PanicHandler) to deal with panics occurring during handling a HTTP request. The router then recovers and lets the PanicHandler log what happened and deliver a nice error page. **Perfect for APIs:** The router design encourages to build sensible, hierarchical RESTful APIs. Moreover it has builtin native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html) and `405 Method Not Allowed` replies. Of course you can also set **custom [NotFound](https://pkg.go.dev/github.com/fasthttp/router#Router.NotFound) and [MethodNotAllowed](https://pkg.go.dev/github.com/fasthttp/router#Router.MethodNotAllowed) handlers** and [**serve static files**](https://pkg.go.dev/github.com/fasthttp/router#Router.ServeFiles). ## Usage This is just a quick introduction, view the [GoDoc](https://pkg.go.dev/github.com/fasthttp/router) for details: Let's start with a trivial example: ```go package main import ( "fmt" "log" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) func Index(ctx *fasthttp.RequestCtx) { ctx.WriteString("Welcome!") } func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "Hello, %s!\n", ctx.UserValue("name")) } func main() { r := router.New() r.GET("/", Index) r.GET("/hello/{name}", Hello) log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) } ``` ### Named parameters As you can see, `{name}` is a _named parameter_. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`. Named parameters only match a single path segment: ``` Pattern: /user/{user} /user/gordon match /user/you match /user/gordon/profile no match /user/ no match Pattern with suffix: /user/{user}_admin /user/gordon_admin match /user/you_admin match /user/you no match /user/gordon/profile no match /user/gordon_admin/profile no match /user/ no match ``` #### Optional parameters If you need define an optional parameters, add `?` at the end of param name. `{name?}` #### Regex validation If you need define a validation, you could use a custom regex for the paramater value, add `:` after the name. For example: `{name:[a-zA-Z]{5}}`. **_Optional parameters and regex validation are compatibles, only add `?` between the name and the regex. For example: `{name?:[a-zA-Z]{5}}`._** ### Catch-All parameters The second type are _catch-all_ parameters and have the form `{name:*}`. Like the name suggests, they match everything. Therefore they must always be at the **end** of the pattern: ``` Pattern: /src/{filepath:*} /src/ match /src/somefile.go match /src/subdir/somefile.go match ``` ## How does it work? The router relies on a tree structure which makes heavy use of _common prefixes_, it is basically a _compact_ [_prefix tree_](https://en.wikipedia.org/wiki/Trie) (or just [_Radix tree_](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like: ``` Priority Path Handle 9 \ *<1> 3 ├s nil 2 |├earch\ *<2> 1 |└upport\ *<3> 2 ├blog\ *<4> 1 | └{post} nil 1 | └\ *<5> 2 ├about-us\ *<6> 1 | └team\ *<7> 1 └contact\ *<8> ``` Every `*` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\{post}\`, where `{post}` is just a placeholder ([_parameter_](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `{post}` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show][benchmark], this works very well and efficient. Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree. For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways: 1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible. 2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right. ``` ├------------ ├--------- ├----- ├---- ├-- ├-- └- ``` ## Why doesn't this work with `http.Handler`? Because fasthttp doesn't provide http.Handler. See this [description](https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp). Fasthttp works with [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler) functions instead of objects implementing Handler interface. So a Router provides a [Handler](https://pkg.go.dev/github.com/fasthttp/router#Router.Handler) interface to implement the fasthttp.ListenAndServe interface. Just try it out for yourself, the usage of Router is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. ## Where can I find Middleware _X_? This package just provides a very efficient request router with a few extra features. The router is just a [`fasthttp.RequestHandler`](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler), you can chain any `fasthttp.RequestHandler` compatible middleware before the router. Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy! Have a look at these middleware examples: - [Auth Middleware](_examples/auth) - [Multi Hosts Middleware](_examples/hosts) ## Chaining with the NotFound handler **NOTE: It might be required to set [Router.HandleMethodNotAllowed](https://pkg.go.dev/github.com/fasthttp/router#Router.HandleMethodNotAllowed) to `false` to avoid problems.** You can use another [fasthttp.RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](https://pkg.go.dev/github.com/fasthttp/router#Router.NotFound) handler. This allows chaining. ### Static files The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets): ```go // Serve static files from the ./public directory r.NotFound = fasthttp.FSHandler("./public", 0) ``` But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/{filepath:*}` or `/files/{filepath:*}`. ## Web Frameworks based on Router If the Router is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the Router package: - [Atreugo](https://github.com/savsgio/atreugo) router-1.5.4/SECURITY.md000066400000000000000000000151161473534360100146440ustar00rootroot00000000000000### TL;DR We use a simplified version of [Golang Security Policy](https://golang.org/security). For example, for now we skip CVE assignment. ### Reporting a Security Bug Please report to us any issues you find. This document explains how to do that and what to expect in return. All security bugs in our releases should be reported by email to oss-security@highload.solutions. This mail is delivered to a small security team. Your email will be acknowledged within 24 hours, and you'll receive a more detailed response to your email within 72 hours indicating the next steps in handling your report. For critical problems, you can encrypt your report using our PGP key (listed below). Please use a descriptive subject line for your report email. After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least every five days. In reality, this is more likely to be every 24-48 hours. If you have not received a reply to your email within 48 hours or you have not heard from the security team for the past five days please contact us by email to developers@highload.solutions or by Telegram message to [our support](https://t.me/highload_support). Please note that developers@highload.solutions list includes all developers, who may be outside our opensource security team. When escalating on this list, please do not disclose the details of the issue. Simply state that you're trying to reach a member of the security team. ### Flagging Existing Issues as Security-related If you believe that an existing issue is security-related, we ask that you send an email to oss-security@highload.solutions. The email should include the issue ID and a short description of why it should be handled according to this security policy. ### Disclosure Process Our project uses the following disclosure process: - Once the security report is received it is assigned a primary handler. This person coordinates the fix and release process. - The issue is confirmed and a list of affected software is determined. - Code is audited to find any potential similar problems. - Fixes are prepared for the two most recent major releases and the head/master revision. These fixes are not yet committed to the public repository. - To notify users, a new issue without security details is submitted to our GitHub repository. - Three working days following this notification, the fixes are applied to the public repository and a new release is issued. - On the date that the fixes are applied, announcement is published in the issue. This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we follow the process described above to ensure that disclosures are handled consistently. ### Receiving Security Updates The best way to receive security announcements is to subscribe ("Watch") to our repository. Any GitHub issues pertaining to a security issue will be prefixed with [security]. ### Comments on This Policy If you have any suggestions to improve this policy, please send an email to oss-security@highload.solutions for discussion. ### PGP Key for oss-security@highload.solutions We accept PGP-encrypted email, but the majority of the security team are not regular PGP users so it's somewhat inconvenient. Please only use PGP for critical security reports. ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFzdjYUBEACa3YN+QVSlnXofUjxr+YrmIaF+da0IUq+TRM4aqUXALsemEdGh yIl7Z6qOOy1d2kPe6t//H9l/92lJ1X7i6aEBK4n/pnPZkwbpy9gGpebgvTZFvcbe mFhF6k1FM35D8TxneJSjizPyGhJPqcr5qccqf8R64TlQx5Ud1JqT2l8P1C5N7gNS lEYXq1h4zBCvTWk1wdeLRRPx7Bn6xrgmyu/k61dLoJDvpvWNATVFDA67oTrPgzTW xtLbbk/xm0mK4a8zMzIpNyz1WkaJW9+4HFXaL+yKlsx7iHe2O7VlGoqS0kdeQup4 1HIw/P7yc0jBlNMLUzpuA6ElYUwESWsnCI71YY1x4rKgI+GqH1mWwgn7tteuXQtb Zj0vEdjK3IKIOSbzbzAvSbDt8F1+o7EMtdy1eUysjKSQgFkDlT6JRmYvEup5/IoG iknh/InQq9RmGFKii6pXWWoltC0ebfCwYOXvymyDdr/hYDqJeHS9Tenpy86Doaaf HGf5nIFAMB2G5ctNpBwzNXR2MAWkeHQgdr5a1xmog0hS125usjnUTet3QeCyo4kd gVouoOroMcqFFUXdYaMH4c3KWz0afhTmIaAsFFOv/eMdadVA4QyExTJf3TAoQ+kH lKDlbOAIxEZWRPDFxMRixaVPQC+VxhBcaQ+yNoaUkM0V2m8u8sDBpzi1OQARAQAB tDxPU1MgU2VjdXJpdHksIEhpZ2hsb2FkIExURCA8b3NzLXNlY3VyaXR5QGhpZ2hs b2FkLnNvbHV0aW9ucz6JAlQEEwEIAD4WIQRljYp380uKq2g8TeqsQcvu+Qp2TAUC XN2NhQIbAwUJB4YfgAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCsQcvu+Qp2 TKmED/96YoQoOjD28blFFrigvAsiNcNNZoX9I0dX1lNpD83fBJf+/9i+x4jqUnI5 5XK/DFTDbhpw8kQBpxS9eEuIYnuo0RdLLp1ctNWTlpwfyHn92mGddl/uBdYHUuUk cjhIQcFaCcWRY+EpamDlv1wmZ83IwBr8Hu5FS+/Msyw1TBvtTRVKW1KoGYMYoXLk BzIglRPwn821B6s4BvK/RJnZkrmHMBZBfYMf+iSMSYd2yPmfT8wbcAjgjLfQa28U gbt4u9xslgKjuM83IqwFfEXBnm7su3OouGWqc+62mQTsbnK65zRFnx6GXRXC1BAi 6m9Tm1PU0IiINz66ainquspkXYeHjd9hTwfR3BdFnzBTRRM01cKMFabWbLj8j0p8 fF4g9cxEdiLrzEF7Yz4WY0mI4Cpw4eJZfsHMc07Jn7QxfJhIoq+rqBOtEmTjnxMh aWeykoXMHlZN4K0ZrAytozVH1D4bugWA9Zuzi9U3F9hrVVABm11yyhd2iSqI6/FR GcCFOCBW1kEJbzoEguub+BV8LDi8ldljHalvur5k/VFhoDBxniYNsKmiCLVCmDWs /nF84hCReAOJt0vDGwqHe3E2BFFPbKwdJLRNkjxBY0c/pvaV+JxbWQmaxDZNeIFV hFcVGp48HNY3qLWZdsQIfT9m1masJFLVuq8Wx7bYs8Et5eFnH7kCDQRc3Y2FARAA 2DJWAxABydyIdCxgFNdqnYyWS46vh2DmLmRMqgasNlD0ozG4S9bszBsgnUI2Xs06 J76kFRh8MMHcu9I4lUKCQzfrA4uHkiOK5wvNCaWP+C6JUYNHsqPwk/ILO3gtQ/Ws LLf/PW3rJZVOZB+WY8iaYc20l5vukTaVw4qbEi9dtLkJvVpNHt//+jayXU6s3ew1 2X5xdwyAZxaxlnzFaY/Xo/qR+bZhVFC0T9pAECnHv9TVhFGp0JE9ipPGnro5xTIS LttdAkzv4AuSVTIgWgTkh8nN8t7STJqfPEv0I12nmmYHMUyTYOurkfskF3jY2x6x 8l02NQ4d5KdC3ReV1j51swrGcZCwsWNp51jnEXKwo+B0NM5OmoRrNJgF2iDgLehs hP00ljU7cB8/1/7kdHZStYaUHICFOFqHzg415FlYm+jpY0nJp/b9BAO0d0/WYnEe Xjihw8EVBAqzEt4kay1BQonZAypeYnGBJr7vNvdiP+mnRwly5qZSGiInxGvtZZFt zL1E3osiF+muQxFcM63BeGdJeYXy+MoczkWa4WNggfcHlGAZkMYiv28zpr4PfrK9 mvj4Nu8s71PE9pPpBoZcNDf9v1sHuu96jDSITsPx5YMvvKZWhzJXFKzk6YgAsNH/ MF0G+/qmKJZpCdvtHKpYM1uHX85H81CwWJFfBPthyD8AEQEAAYkCPAQYAQgAJhYh BGWNinfzS4qraDxN6qxBy+75CnZMBQJc3Y2FAhsMBQkHhh+AAAoJEKxBy+75CnZM Rn8P/RyL1bhU4Q4WpvmlkepCAwNA0G3QvnKcSZNHEPE5h7H3IyrA/qy16A9eOsgm sthsHYlo5A5lRIy4wPHkFCClMrMHdKuoS72//qgw+oOrBcwb7Te+Nas+ewhaJ7N9 vAX06vDH9bLl52CPbtats5+eBpePgP3HDPxd7CWHxq9bzJTbzqsTkN7JvoovR2dP itPJDij7QYLYVEM1t7QxUVpVwAjDi/kCtC9ts5L+V0snF2n3bHZvu04EXdpvxOQI pG/7Q+/WoI8NU6Bb/FA3tJGYIhSwI3SY+5XV/TAZttZaYSh2SD8vhc+eo+gW9sAN xa+VESBQCht9+tKIwEwHs1efoRgFdbwwJ2c+33+XydQ6yjdXoX1mn2uyCr82jorZ xTzbkY04zr7oZ+0fLpouOFg/mrSL4w2bWEhdHuyoVthLBjnRme0wXCaS3g3mYdLG nSUkogOGOOvvvBtoq/vfx0Eu79piUtw5D8yQSrxLDuz8GxCrVRZ0tYIHb26aTE9G cDsW/Lg5PjcY/LgVNEWOxDQDFVurlImnlVJFb3q+NrWvPbgeIEWwJDCay/z25SEH k3bSOXLp8YGRnlkWUmoeL4g/CCK52iAAlfscZNoKMILhBnbCoD657jpa5GQKJj/U Q8kjgr7kwV/RSosNV9HCPj30mVyiCQ1xg+ZLzMKXVCuBWd+G =lnt2 -----END PGP PUBLIC KEY BLOCK----- ``` router-1.5.4/_examples/000077500000000000000000000000001473534360100150245ustar00rootroot00000000000000router-1.5.4/_examples/auth/000077500000000000000000000000001473534360100157655ustar00rootroot00000000000000router-1.5.4/_examples/auth/README.md000066400000000000000000000077271473534360100172610ustar00rootroot00000000000000# Example of Router These examples show you the usage of `router`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. ### Basic Authentication This Basic Authentication example uses `simple-scrypt` for password hashing: go get -u github.com/elithrar/simple-scrypt Password hashing is used so that if your data store is compromised, the attackers will only have access to hashed passwords, which (if the hash is not itself compromised) will not be able to revert to the original plain text password. After you have hashed the password and stored the hash in a data store, you should throw away the original plain-text password. The next time the user attempts to log in, their password will be safely hashed and compared to the saved hash. If the hashes match, then the user will be accepted. Only use constant time comparison functions that are built into your hash library to compare secret strings like passwords or hashes to prevent timing attacks. ```go package main import ( "encoding/base64" "fmt" "log" "strings" "github.com/fasthttp/router" "github.com/elithrar/simple-scrypt" "github.com/valyala/fasthttp" ) // basicAuth returns the username and password provided in the request's // Authorization header, if the request uses HTTP Basic Authentication. // See RFC 2617, Section 2. func basicAuth(ctx *fasthttp.RequestCtx) (username, password string, ok bool) { auth := ctx.Request.Header.Peek("Authorization") if auth == nil { return } return parseBasicAuth(string(auth)) } // parseBasicAuth parses an HTTP Basic Authentication string. // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). func parseBasicAuth(auth string) (username, password string, ok bool) { const prefix = "Basic " if !strings.HasPrefix(auth, prefix) { return } c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) if err != nil { return } cs := string(c) s := strings.IndexByte(cs, ':') if s < 0 { return } return cs[:s], cs[s+1:], true } // BasicAuth is the basic auth handler func BasicAuth(h fasthttp.RequestHandler, requiredUser string, requiredPasswordHash []byte) fasthttp.RequestHandler { return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { // Get the Basic Authentication credentials user, password, hasAuth := basicAuth(ctx) // WARNING: // DO NOT use plain-text passwords for real apps. // A simple string comparison using == is vulnerable to a timing attack. // Instead, use the hash comparison function found in your hash library. // This example uses scrypt, which is a solid choice for secure hashing: // go get -u github.com/elithrar/simple-scrypt if hasAuth && user == requiredUser { // Uses the parameters from the existing derived key. Return an error if they don't match. err := scrypt.CompareHashAndPassword(requiredPasswordHash, []byte(password)) if err != nil { // log error and request Basic Authentication again below. log.Fatal(err) } else { // Delegate request to the given handle h(ctx) return } } // Request Basic Authentication otherwise ctx.Error(fasthttp.StatusMessage(fasthttp.StatusUnauthorized), fasthttp.StatusUnauthorized) ctx.Response.Header.Set("WWW-Authenticate", "Basic realm=Restricted") }) } // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Not protected!\n") } // Protected is the Protected handler func Protected(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Protected!\n") } func main() { user := "gordon" pass := "secret!" // generate a hashed password from the password above: hashedPassword, err := scrypt.GenerateFromPassword([]byte(pass), scrypt.DefaultParams) if err != nil { log.Fatal(err) } r := router.New() r.GET("/", Index) r.GET("/protected/", BasicAuth(Protected, user, hashedPassword)) log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) } ``` router-1.5.4/_examples/auth/auth.go000066400000000000000000000055511473534360100172630ustar00rootroot00000000000000package main import ( "encoding/base64" "fmt" "log" "strings" scrypt "github.com/elithrar/simple-scrypt" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) // basicAuth returns the username and password provided in the request's // Authorization header, if the request uses HTTP Basic Authentication. // See RFC 2617, Section 2. func basicAuth(ctx *fasthttp.RequestCtx) (username, password string, ok bool) { auth := ctx.Request.Header.Peek("Authorization") if auth == nil { return } return parseBasicAuth(string(auth)) } // parseBasicAuth parses an HTTP Basic Authentication string. // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). func parseBasicAuth(auth string) (username, password string, ok bool) { const prefix = "Basic " if !strings.HasPrefix(auth, prefix) { return } c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) if err != nil { return } cs := string(c) s := strings.IndexByte(cs, ':') if s < 0 { return } return cs[:s], cs[s+1:], true } // BasicAuth is the basic auth handler func BasicAuth(h fasthttp.RequestHandler, requiredUser string, requiredPasswordHash []byte) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { // Get the Basic Authentication credentials user, password, hasAuth := basicAuth(ctx) // WARNING: // DO NOT use plain-text passwords for real apps. // A simple string comparison using == is vulnerable to a timing attack. // Instead, use the hash comparison function found in your hash library. // This example uses scrypt, which is a solid choice for secure hashing: // go get -u github.com/elithrar/simple-scrypt if hasAuth && user == requiredUser { // Uses the parameters from the existing derived key. Return an error if they don't match. err := scrypt.CompareHashAndPassword(requiredPasswordHash, []byte(password)) if err != nil { // log error and request Basic Authentication again below. log.Fatal(err) } else { // Delegate request to the given handle h(ctx) return } } // Request Basic Authentication otherwise ctx.Error(fasthttp.StatusMessage(fasthttp.StatusUnauthorized), fasthttp.StatusUnauthorized) ctx.Response.Header.Set("WWW-Authenticate", "Basic realm=Restricted") } } // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Not protected!\n") } // Protected is the Protected handler func Protected(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Protected!\n") } func main() { user := "gordon" pass := "secret!" // generate a hashed password from the password above: hashedPassword, err := scrypt.GenerateFromPassword([]byte(pass), scrypt.DefaultParams) if err != nil { log.Fatal(err) } r := router.New() r.GET("/", Index) r.GET("/protected/", BasicAuth(Protected, user, hashedPassword)) log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) } router-1.5.4/_examples/basic/000077500000000000000000000000001473534360100161055ustar00rootroot00000000000000router-1.5.4/_examples/basic/README.md000066400000000000000000000025771473534360100173770ustar00rootroot00000000000000# Example of Router These examples show you the usage of `router`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. ### Basic example This is just a quick introduction, view the [GoDoc](https://pkg.go.dev/github.com/fasthttp/router) for details. Let's start with a trivial example: ```go package main import ( "fmt" "log" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Welcome!\n") } // Hello is the Hello handler func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) } // MultiParams is the multi params handler func MultiParams(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hi, %s, %s!\n", ctx.UserValue("name"), ctx.UserValue("word")) } // QueryArgs is used for uri query args test #11: // if the req uri is /ping?name=foo, output: Pong! foo // if the req uri is /piNg?name=foo, redirect to /ping, output: Pong! func QueryArgs(ctx *fasthttp.RequestCtx) { name := ctx.QueryArgs().Peek("name") fmt.Fprintf(ctx, "Pong! %s\n", string(name)) } func main() { r := router.New() r.GET("/", Index) r.GET("/hello/{name}", Hello) r.GET("/multi/{name}/{word}", MultiParams) r.GET("/ping", QueryArgs) log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) } ``` router-1.5.4/_examples/basic/basic.go000066400000000000000000000024171473534360100175210ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Welcome!\n") } // Hello is the Hello handler func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) } // MultiParams is the multi params handler func MultiParams(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hi, %s, %s!\n", ctx.UserValue("name"), ctx.UserValue("word")) } // RegexParams is the params handler with regex validation func RegexParams(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hi, %s\n", ctx.UserValue("name")) } // QueryArgs is used for uri query args test #11: // if the req uri is /ping?name=foo, output: Pong! foo // if the req uri is /piNg?name=foo, redirect to /ping, output: Pong! func QueryArgs(ctx *fasthttp.RequestCtx) { name := ctx.QueryArgs().Peek("name") fmt.Fprintf(ctx, "Pong! %s\n", string(name)) } func main() { r := router.New() r.GET("/", Index) r.GET("/hello/{name}", Hello) r.GET("/multi/{name}/{word}", MultiParams) r.GET("/regex/{name:[a-zA-Z]+}/test", RegexParams) r.GET("/optional/{name?:[a-zA-Z]+}/{word?}", MultiParams) r.GET("/ping", QueryArgs) log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler)) } router-1.5.4/_examples/hosts/000077500000000000000000000000001473534360100161645ustar00rootroot00000000000000router-1.5.4/_examples/hosts/README.md000066400000000000000000000033651473534360100174520ustar00rootroot00000000000000# Example of Router These examples show you the usage of `router`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. ### Multi-domain / Sub-domains Here is a quick example: Does your server serve multiple domains / hosts? You want to use sub-domains? Define a router per host! ```go package main import ( "fmt" "log" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Welcome!\n") } // Hello is the Hello handler func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) } // HostSwitch is the host-handler map // We need an object that implements the fasthttp.RequestHandler interface. // We just use a map here, in which we map host names (with port) to fasthttp.RequestHandlers type HostSwitch map[string]fasthttp.RequestHandler // CheckHost Implement a CheckHost method on our new type func (hs HostSwitch) CheckHost(ctx *fasthttp.RequestCtx) { // Check if a http.Handler is registered for the given host. // If yes, use it to handle the request. if handler := hs[string(ctx.Host())]; handler != nil { handler(ctx) } else { // Handle host names for wich no handler is registered ctx.Error("Forbidden", 403) // Or Redirect? } } func main() { // Initialize a router as usual r := router.New() r.GET("/", Index) r.GET("/hello/{name}", Hello) // Make a new HostSwitch and insert the router (our http handler) // for example.com and port 12345 hs := make(HostSwitch) hs["example.com:12345"] = r.Handler // Use the HostSwitch to listen and serve on port 12345 log.Fatal(fasthttp.ListenAndServe(":12345", hs.CheckHost)) } ``` router-1.5.4/_examples/hosts/hosts.go000066400000000000000000000025761473534360100176650ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) // Index is the index handler func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(ctx, "Welcome!\n") } // Hello is the Hello handler func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) } // HostSwitch is the host-handler map // We need an object that implements the fasthttp.RequestHandler interface. // We just use a map here, in which we map host names (with port) to fasthttp.RequestHandlers type HostSwitch map[string]fasthttp.RequestHandler // CheckHost Implement a CheckHost method on our new type func (hs HostSwitch) CheckHost(ctx *fasthttp.RequestCtx) { // Check if a http.Handler is registered for the given host. // If yes, use it to handle the request. if handler := hs[string(ctx.Host())]; handler != nil { handler(ctx) } else { // Handle host names for which no handler is registered ctx.Error("Forbidden", 403) // Or Redirect? } } func main() { // Initialize a router as usual r := router.New() r.GET("/", Index) r.GET("/hello/{name}", Hello) // Make a new HostSwitch and insert the router (our http handler) // for example.com and port 12345 hs := make(HostSwitch) hs["example.com:12345"] = r.Handler // Use the HostSwitch to listen and serve on port 12345 log.Fatal(fasthttp.ListenAndServe(":12345", hs.CheckHost)) } router-1.5.4/doc.go000066400000000000000000000043621473534360100141500ustar00rootroot00000000000000/* Package router is a trie based high performance HTTP request router. A trivial example is: package main import ( "fmt" "log" "github.com/fasthttp/router" ) func Index(ctx *fasthttp.RequestCtx) { fmt.Fprint(w, "Welcome!\n") } func Hello(ctx *fasthttp.RequestCtx) { fmt.Fprintf(w, "hello, %s!\n", ctx.UserValue("name")) } func main() { r := router.New() r.GET("/", 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 handler is registered for this path and method, the router delegates the request to that function. For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS 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 saved in ctx.UserValue(), consisting each of a key and a value. The slice is passed to the Handle func as a third parameter. To retrieve the value of a parameter,gets by the name of the parameter user := ctx.UserValue("user") // defined by {user} or {user:*} */ package router router-1.5.4/go.mod000066400000000000000000000005141473534360100141550ustar00rootroot00000000000000module github.com/fasthttp/router go 1.21 toolchain go1.23.4 require ( github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.58.0 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/klauspost/compress v1.17.11 // indirect ) router-1.5.4/go.sum000066400000000000000000000021441473534360100142030ustar00rootroot00000000000000github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 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/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= router-1.5.4/group.go000066400000000000000000000112571473534360100145400ustar00rootroot00000000000000package router import ( "io/fs" "github.com/valyala/fasthttp" ) // Group returns a new group. // Path auto-correction, including trailing slashes, is enabled by default. func (g *Group) Group(path string) *Group { validatePath(path) if len(g.prefix) > 0 && path == "/" { return g } return g.router.Group(g.prefix + path) } // GET is a shortcut for group.Handle(fasthttp.MethodGet, path, handler) func (g *Group) GET(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.GET(g.prefix+path, handler) } // HEAD is a shortcut for group.Handle(fasthttp.MethodHead, path, handler) func (g *Group) HEAD(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.HEAD(g.prefix+path, handler) } // POST is a shortcut for group.Handle(fasthttp.MethodPost, path, handler) func (g *Group) POST(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.POST(g.prefix+path, handler) } // PUT is a shortcut for group.Handle(fasthttp.MethodPut, path, handler) func (g *Group) PUT(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.PUT(g.prefix+path, handler) } // PATCH is a shortcut for group.Handle(fasthttp.MethodPatch, path, handler) func (g *Group) PATCH(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.PATCH(g.prefix+path, handler) } // DELETE is a shortcut for group.Handle(fasthttp.MethodDelete, path, handler) func (g *Group) DELETE(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.DELETE(g.prefix+path, handler) } // OPTIONS is a shortcut for group.Handle(fasthttp.MethodOptions, path, handler) func (g *Group) CONNECT(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.CONNECT(g.prefix+path, handler) } // OPTIONS is a shortcut for group.Handle(fasthttp.MethodOptions, path, handler) func (g *Group) OPTIONS(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.OPTIONS(g.prefix+path, handler) } // OPTIONS is a shortcut for group.Handle(fasthttp.MethodOptions, path, handler) func (g *Group) TRACE(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.TRACE(g.prefix+path, handler) } // ANY is a shortcut for group.Handle(router.MethodWild, path, handler) // // WARNING: Use only for routes where the request method is not important func (g *Group) ANY(path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.ANY(g.prefix+path, handler) } // ServeFiles serves files from the given file system root path. // 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 fasthttp.FSHandler is used, therefore http.NotFound is used instead // Use: // // router.ServeFiles("/src/{filepath:*}", "./") func (g *Group) ServeFiles(path string, rootPath string) { validatePath(path) g.router.ServeFiles(g.prefix+path, rootPath) } // ServeFS serves files from the given file system. // 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 fasthttp.FSHandler is used, therefore http.NotFound is used instead // Use: // // router.ServeFS("/src/{filepath:*}", myFilesystem) func (g *Group) ServeFS(path string, filesystem fs.FS) { validatePath(path) g.router.ServeFS(g.prefix+path, filesystem) } // 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 fasthttp.FSHandler is used, therefore http.NotFound is used instead // of the Router's NotFound handler. // Use: // // router.ServeFilesCustom("/src/{filepath:*}", *customFS) func (g *Group) ServeFilesCustom(path string, fs *fasthttp.FS) { validatePath(path) g.router.ServeFilesCustom(g.prefix+path, fs) } // Handle registers a new request handler 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 (g *Group) Handle(method, path string, handler fasthttp.RequestHandler) { validatePath(path) g.router.Handle(method, g.prefix+path, handler) } router-1.5.4/group_test.go000066400000000000000000000133131473534360100155720ustar00rootroot00000000000000package router import ( "bufio" "reflect" "strings" "testing" "github.com/valyala/fasthttp" ) type routerGrouper interface { Group(string) *Group ServeFiles(path string, rootPath string) ServeFilesCustom(path string, fs *fasthttp.FS) } func assertGroup(t *testing.T, gs ...routerGrouper) { for i, g := range gs { g2 := g.Group("/") v1 := reflect.ValueOf(g) v2 := reflect.ValueOf(g2) if v1.String() != v2.String() { // router -> group if v1.Pointer() == v2.Pointer() { t.Errorf("[%d] equal pointers: %p == %p", i, g, g2) } } else { // group -> subgroup if v1.Pointer() != v2.Pointer() { t.Errorf("[%d] mismatch pointers: %p != %p", i, g, g2) } } if err := catchPanic(func() { g.Group("v999") }); err == nil { t.Error("an error was expected when a path does not begin with slash") } if err := catchPanic(func() { g.Group("/v999/") }); err == nil { t.Error("an error was expected when a path has a trailing slash") } if err := catchPanic(func() { g.Group("") }); err == nil { t.Error("an error was expected with an empty path") } if err := catchPanic(func() { g.ServeFiles("static/{filepath:*}", "./") }); err == nil { t.Error("an error was expected when a path does not begin with slash") } if err := catchPanic(func() { g.ServeFilesCustom("", &fasthttp.FS{Root: "./"}) }); err == nil { t.Error("an error was expected with an empty path") } } } func TestGroup(t *testing.T) { r1 := New() r2 := r1.Group("/boo") r3 := r1.Group("/goo") r4 := r1.Group("/moo") r5 := r4.Group("/foo") r6 := r5.Group("/foo") assertGroup(t, r1, r2, r3, r4, r5, r6) hit := false r1.POST("/foo", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r2.POST("/bar", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r3.POST("/bar", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r4.POST("/bar", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r5.POST("/bar", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r6.POST("/bar", func(ctx *fasthttp.RequestCtx) { hit = true ctx.SetStatusCode(fasthttp.StatusOK) }) r6.ServeFiles("/static/{filepath:*}", "./") r6.ServeFS("/static/fs/{filepath:*}", fsTestFilesystem) r6.ServeFilesCustom("/custom/static/{filepath:*}", &fasthttp.FS{Root: "./"}) uris := []string{ "POST /foo HTTP/1.1\r\n\r\n", // testing router group - r2 (grouped from r1) "POST /boo/bar HTTP/1.1\r\n\r\n", // testing multiple router group - r3 (grouped from r1) "POST /goo/bar HTTP/1.1\r\n\r\n", // testing multiple router group - r4 (grouped from r1) "POST /moo/bar HTTP/1.1\r\n\r\n", // testing sub-router group - r5 (grouped from r4) "POST /moo/foo/bar HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) "POST /moo/foo/foo/bar HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) to serve files "GET /moo/foo/foo/static/router.go HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) to serve fs "GET /moo/foo/foo/static/fs/LICENSE HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) to serve files with custom settings "GET /moo/foo/foo/custom/static/router.go HTTP/1.1\r\n\r\n", } for _, uri := range uris { hit = false assertWithTestServer(t, uri, r1.Handler, func(rw *readWriter) { br := bufio.NewReader(&rw.w) var resp fasthttp.Response if err := resp.Read(br); err != nil { t.Fatalf("Unexpected error when reading response: %s", err) } if !(resp.Header.StatusCode() == fasthttp.StatusOK) { t.Fatalf("Status code %d, want %d", resp.Header.StatusCode(), fasthttp.StatusOK) } if !strings.Contains(uri, "static") && !hit { t.Fatalf("Regular routing failed with router chaining. %s", uri) } }) } assertWithTestServer(t, "POST /qax HTTP/1.1\r\n\r\n", r1.Handler, func(rw *readWriter) { br := bufio.NewReader(&rw.w) var resp fasthttp.Response if err := resp.Read(br); err != nil { t.Fatalf("Unexpected error when reading response: %s", err) } if !(resp.Header.StatusCode() == fasthttp.StatusNotFound) { t.Errorf("NotFound behavior failed with router chaining.") t.FailNow() } }) } func TestGroup_shortcutsAndHandle(t *testing.T) { r := New() g := r.Group("/v1") shortcuts := []func(path string, handler fasthttp.RequestHandler){ g.GET, g.HEAD, g.POST, g.PUT, g.PATCH, g.DELETE, g.CONNECT, g.OPTIONS, g.TRACE, g.ANY, } for _, fn := range shortcuts { fn("/bar", func(_ *fasthttp.RequestCtx) {}) if err := catchPanic(func() { fn("buzz", func(_ *fasthttp.RequestCtx) {}) }); err == nil { t.Error("an error was expected when a path does not begin with slash") } if err := catchPanic(func() { fn("", func(_ *fasthttp.RequestCtx) {}) }); err == nil { t.Error("an error was expected with an empty path") } } methods := httpMethods[:len(httpMethods)-1] // Avoid customs methods for _, method := range methods { h, _ := r.Lookup(method, "/v1/bar", nil) if h == nil { t.Errorf("Bad shorcurt") } } g2 := g.Group("/foo") for _, method := range httpMethods { g2.Handle(method, "/bar", func(_ *fasthttp.RequestCtx) {}) if err := catchPanic(func() { g2.Handle(method, "buzz", func(_ *fasthttp.RequestCtx) {}) }); err == nil { t.Error("an error was expected when a path does not begin with slash") } if err := catchPanic(func() { g2.Handle(method, "", func(_ *fasthttp.RequestCtx) {}) }); err == nil { t.Error("an error was expected with an empty path") } h, _ := r.Lookup(method, "/v1/foo/bar", nil) if h == nil { t.Errorf("Bad shorcurt") } } } router-1.5.4/path.go000066400000000000000000000030401473534360100143270ustar00rootroot00000000000000package router import ( "strings" gstrings "github.com/savsgio/gotils/strings" ) // cleanPath removes the '.' if it is the last character of the route func cleanPath(path string) string { return strings.TrimSuffix(path, ".") } // getOptionalPaths returns all possible paths when the original path // has optional arguments func getOptionalPaths(path string) []string { paths := make([]string, 0) start := 0 walk: for { if start >= len(path) { return paths } c := path[start] start++ if c != '{' { continue } newPath := "" hasRegex := false questionMarkIndex := -1 brackets := 0 for end, c := range []byte(path[start:]) { switch c { case '{': brackets++ case '}': if brackets > 0 { brackets-- continue } else if questionMarkIndex == -1 { continue walk } end++ newPath += path[questionMarkIndex+1 : start+end] path = path[:questionMarkIndex] + path[questionMarkIndex+1:] // remove '?' paths = append(paths, newPath) start += end - 1 continue walk case ':': hasRegex = true case '?': if hasRegex { continue } questionMarkIndex = start + end newPath += path[:questionMarkIndex] if len(path[:start-2]) == 0 { // include the root slash because the param is in the first segment paths = append(paths, "/") } else if !gstrings.Include(paths, path[:start-2]) { // include the path without the wildcard // -2 due to remove the '/' and '{' paths = append(paths, path[:start-2]) } } } } } router-1.5.4/path_test.go000066400000000000000000000074241473534360100154000ustar00rootroot00000000000000package router import ( "reflect" "runtime" "testing" "github.com/savsgio/gotils/strings" "github.com/valyala/fasthttp" ) type cleanPathTest struct { path, result string } var cleanTests = []cleanPathTest{ // Already clean {"/", "/"}, {"/abc", "/abc"}, {"/a/b/c", "/a/b/c"}, {"/abc/", "/abc/"}, {"/a/b/c/", "/a/b/c/"}, // missing root {"", "/"}, {"a/", "/a/"}, {"abc", "/abc"}, {"abc/def", "/abc/def"}, {"a/b/c", "/a/b/c"}, // Remove doubled slash {"//", "/"}, {"/abc//", "/abc/"}, {"/abc/def//", "/abc/def/"}, {"/a/b/c//", "/a/b/c/"}, {"/abc//def//ghi", "/abc/def/ghi"}, {"//abc", "/abc"}, {"///abc", "/abc"}, {"//abc//", "/abc/"}, // Remove . elements {".", "/"}, {"./", "/"}, {"/abc/./def", "/abc/def"}, {"/./abc/def", "/abc/def"}, {"/abc/.", "/abc/"}, // Remove .. elements {"..", "/"}, {"../", "/"}, {"../../", "/"}, {"../..", "/"}, {"../../abc", "/abc"}, {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, {"/abc/def/../ghi/../jkl", "/abc/jkl"}, {"/abc/def/..", "/abc/"}, {"/abc/def/../..", "/"}, {"/abc/def/../../..", "/"}, {"/abc/def/../../..", "/"}, {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, // Combinations {"abc/./../def", "/def"}, {"abc//./../def", "/def"}, {"abc/../../././../def", "/def"}, } func Test_cleanPath(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } req := new(fasthttp.Request) uri := req.URI() for _, test := range cleanTests { uri.SetPath(test.path) if s := cleanPath(string(uri.Path())); s != test.result { t.Errorf("cleanPath(%q) = %q, want %q", test.path, s, test.result) } uri.SetPath(test.result) if s := cleanPath(string(uri.Path())); s != test.result { t.Errorf("cleanPath(%q) = %q, want %q", test.result, s, test.result) } } } func TestGetOptionalPath(t *testing.T) { handler := func(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusOK) } expected := []struct { path string tsr bool handler fasthttp.RequestHandler }{ {"/show/{name}", false, handler}, {"/show/{name}/", true, nil}, {"/show/{name}/{surname}", false, handler}, {"/show/{name}/{surname}/", true, nil}, {"/show/{name}/{surname}/at", false, handler}, {"/show/{name}/{surname}/at/", true, nil}, {"/show/{name}/{surname}/at/{address}", false, handler}, {"/show/{name}/{surname}/at/{address}/", true, nil}, {"/show/{name}/{surname}/at/{address}/{id}", false, handler}, {"/show/{name}/{surname}/at/{address}/{id}/", true, nil}, {"/show/{name}/{surname}/at/{address}/{id}/{phone:.*}", false, handler}, {"/show/{name}/{surname}/at/{address}/{id}/{phone:.*}/", true, nil}, } r := New() r.GET("/show/{name}/{surname?}/at/{address?}/{id}/{phone?:.*}", handler) for _, e := range expected { ctx := new(fasthttp.RequestCtx) h, tsr := r.Lookup("GET", e.path, ctx) if tsr != e.tsr { t.Errorf("TSR (path: %s) == %v, want %v", e.path, tsr, e.tsr) } if reflect.ValueOf(h).Pointer() != reflect.ValueOf(e.handler).Pointer() { t.Errorf("Handler (path: %s) == %p, want %p", e.path, h, e.handler) } } tests := []struct { path string optionalPaths []string }{ {"/hello", nil}, {"/{name}", nil}, {"/{name?:[a-zA-Z]{5}}", []string{"/", "/{name:[a-zA-Z]{5}}"}}, {"/{filepath:^(?!api).*}", nil}, {"/static/{filepath?:^(?!api).*}", []string{"/static", "/static/{filepath:^(?!api).*}"}}, {"/show/{name?}", []string{"/show", "/show/{name}"}}, } for _, test := range tests { optionalPaths := getOptionalPaths(test.path) if len(optionalPaths) != len(test.optionalPaths) { t.Errorf("getOptionalPaths() len == %d, want %d", len(optionalPaths), len(test.optionalPaths)) } for _, wantPath := range test.optionalPaths { if !strings.Include(optionalPaths, wantPath) { t.Errorf("The optional path is not returned for '%s': %s", test.path, wantPath) } } } } router-1.5.4/radix/000077500000000000000000000000001473534360100141565ustar00rootroot00000000000000router-1.5.4/radix/conts.go000066400000000000000000000001101473534360100156230ustar00rootroot00000000000000package radix const ( root nodeType = iota static param wildcard ) router-1.5.4/radix/errors.go000066400000000000000000000015001473534360100160150ustar00rootroot00000000000000package radix import ( "fmt" ) const ( errSetHandler = "a handler is already registered for path '%s'" errSetWildcardHandler = "a wildcard handler is already registered for path '%s'" errWildPathConflict = "'%s' in new path '%s' conflicts with existing wild path '%s' in existing prefix '%s'" errWildcardConflict = "'%s' in new path '%s' conflicts with existing wildcard '%s' in existing prefix '%s'" errWildcardSlash = "no / before wildcard in path '%s'" errWildcardNotAtEnd = "wildcard routes are only allowed at the end of the path in path '%s'" ) type radixError struct { msg string params []interface{} } func (err radixError) Error() string { return fmt.Sprintf(err.msg, err.params...) } func newRadixError(msg string, params ...interface{}) radixError { return radixError{msg, params} } router-1.5.4/radix/node.go000066400000000000000000000236531473534360100154430ustar00rootroot00000000000000package radix import ( "sort" "strings" gstrings "github.com/savsgio/gotils/strings" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) func newNode(path string) *node { return &node{ nType: static, path: path, } } // conflict raises a panic with some details func (n *nodeWildcard) conflict(path, fullPath string) error { prefix := fullPath[:strings.LastIndex(fullPath, path)] + n.path return newRadixError(errWildcardConflict, path, fullPath, n.path, prefix) } // wildPathConflict raises a panic with some details func (n *node) wildPathConflict(path, fullPath string) error { pathSeg := strings.SplitN(path, "/", 2)[0] prefix := fullPath[:strings.LastIndex(fullPath, path)] + n.path return newRadixError(errWildPathConflict, pathSeg, fullPath, n.path, prefix) } // clone clones the current node in a new pointer func (n node) clone() *node { cloneNode := new(node) cloneNode.nType = n.nType cloneNode.path = n.path cloneNode.tsr = n.tsr cloneNode.handler = n.handler if len(n.children) > 0 { cloneNode.children = make([]*node, len(n.children)) for i, child := range n.children { cloneNode.children[i] = child.clone() } } if n.wildcard != nil { cloneNode.wildcard = &nodeWildcard{ path: n.wildcard.path, paramKey: n.wildcard.paramKey, handler: n.wildcard.handler, } } if len(n.paramKeys) > 0 { cloneNode.paramKeys = make([]string, len(n.paramKeys)) copy(cloneNode.paramKeys, n.paramKeys) } cloneNode.paramRegex = n.paramRegex return cloneNode } func (n *node) split(i int) { cloneChild := n.clone() cloneChild.nType = static cloneChild.path = cloneChild.path[i:] cloneChild.paramKeys = nil cloneChild.paramRegex = nil n.path = n.path[:i] n.handler = nil n.tsr = false n.wildcard = nil n.children = append(n.children[:0], cloneChild) } func (n *node) findEndIndexAndValues(path string) (int, []string) { index := n.paramRegex.FindStringSubmatchIndex(path) if len(index) == 0 || index[0] != 0 { return -1, nil } end := index[1] index = index[2:] values := make([]string, len(index)/2) i := 0 for j := range index { if (j+1)%2 != 0 { continue } values[i] = gstrings.Copy(path[index[j-1]:index[j]]) i++ } return end, values } func (n *node) setHandler(handler fasthttp.RequestHandler, fullPath string) (*node, error) { if n.handler != nil || n.tsr { return n, newRadixError(errSetHandler, fullPath) } n.handler = handler foundTSR := false // Set TSR in method for i := range n.children { child := n.children[i] if child.path != "/" { continue } child.tsr = true foundTSR = true break } if n.path != "/" && !foundTSR { if strings.HasSuffix(n.path, "/") { n.split(len(n.path) - 1) n.tsr = true } else { childTSR := newNode("/") childTSR.tsr = true n.children = append(n.children, childTSR) } } return n, nil } func (n *node) insert(path, fullPath string, handler fasthttp.RequestHandler) (*node, error) { end := segmentEndIndex(path, true) child := newNode(path) wp := findWildPath(path, fullPath) if wp != nil { j := end if wp.start > 0 { j = wp.start } child.path = path[:j] if wp.start > 0 { n.children = append(n.children, child) return child.insert(path[j:], fullPath, handler) } switch wp.pType { case param: n.hasWildChild = true child.nType = wp.pType child.paramKeys = wp.keys child.paramRegex = wp.regex case wildcard: if len(path) == end && n.path[len(n.path)-1] != '/' { return nil, newRadixError(errWildcardSlash, fullPath) } else if len(path) != end { return nil, newRadixError(errWildcardNotAtEnd, fullPath) } if n.path != "/" && n.path[len(n.path)-1] == '/' { n.split(len(n.path) - 1) n.tsr = true n = n.children[0] } if n.wildcard != nil { if n.wildcard.path == path { return n, newRadixError(errSetWildcardHandler, fullPath) } return nil, n.wildcard.conflict(path, fullPath) } n.wildcard = &nodeWildcard{ path: wp.path, paramKey: wp.keys[0], handler: handler, } return n, nil } path = path[wp.end:] if len(path) > 0 { n.children = append(n.children, child) return child.insert(path, fullPath, handler) } } child.handler = handler n.children = append(n.children, child) if child.path == "/" { // Add TSR when split a edge and the remain path to insert is "/" n.tsr = true } else if strings.HasSuffix(child.path, "/") { child.split(len(child.path) - 1) child.tsr = true } else { childTSR := newNode("/") childTSR.tsr = true child.children = append(child.children, childTSR) } return child, nil } // add adds the handler to node for the given path func (n *node) add(path, fullPath string, handler fasthttp.RequestHandler) (*node, error) { if len(path) == 0 { return n.setHandler(handler, fullPath) } for _, child := range n.children { i := longestCommonPrefix(path, child.path) if i == 0 { continue } switch child.nType { case static: if len(child.path) > i { child.split(i) } if len(path) > i { return child.add(path[i:], fullPath, handler) } case param: wp := findWildPath(path, fullPath) isParam := wp.start == 0 && wp.pType == param hasHandler := child.handler != nil || handler == nil if len(path) == wp.end && isParam && hasHandler { // The current segment is a param and it's duplicated if child.path == path { return child, newRadixError(errSetHandler, fullPath) } return nil, child.wildPathConflict(path, fullPath) } if len(path) > i { if child.path == wp.path { return child.add(path[i:], fullPath, handler) } return n.insert(path, fullPath, handler) } } if path == "/" { n.tsr = true } return child.setHandler(handler, fullPath) } return n.insert(path, fullPath, handler) } func (n *node) getFromChild(path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) { for _, child := range n.children { switch child.nType { case static: // Checks if the first byte is equal // It's faster than compare strings if path[0] != child.path[0] { continue } if len(path) > len(child.path) { if path[:len(child.path)] != child.path { continue } h, tsr := child.getFromChild(path[len(child.path):], ctx) if h != nil || tsr { return h, tsr } } else if path == child.path { switch { case child.tsr: return nil, true case child.handler != nil: return child.handler, false case child.wildcard != nil: if ctx != nil { ctx.SetUserValue(child.wildcard.paramKey, "") } return child.wildcard.handler, false } return nil, false } case param: end := segmentEndIndex(path, false) values := []string{gstrings.Copy(path[:end])} if child.paramRegex != nil { end, values = child.findEndIndexAndValues(path[:end]) if end == -1 { continue } } if len(path) > end { h, tsr := child.getFromChild(path[end:], ctx) if tsr { return nil, tsr } else if h != nil { if ctx != nil { for i, key := range child.paramKeys { ctx.SetUserValue(key, values[i]) } } return h, false } } else if len(path) == end { switch { case child.tsr: return nil, true case child.handler == nil: // try another child continue case ctx != nil: for i, key := range child.paramKeys { ctx.SetUserValue(key, values[i]) } } return child.handler, false } default: panic("invalid node type") } } if n.wildcard != nil { if ctx != nil { ctx.SetUserValue(n.wildcard.paramKey, gstrings.Copy(path)) } return n.wildcard.handler, false } return nil, false } func (n *node) find(path string, buf *bytebufferpool.ByteBuffer) (bool, bool) { if len(path) > len(n.path) { if !strings.EqualFold(path[:len(n.path)], n.path) { return false, false } path = path[len(n.path):] buf.WriteString(n.path) found, tsr := n.findFromChild(path, buf) if found { return found, tsr } bufferRemoveString(buf, n.path) } else if strings.EqualFold(path, n.path) { buf.WriteString(n.path) if n.tsr { if n.path == "/" { bufferRemoveString(buf, n.path) } else { buf.WriteByte('/') } return true, true } if n.handler != nil { return true, false } else { bufferRemoveString(buf, n.path) } } return false, false } func (n *node) findFromChild(path string, buf *bytebufferpool.ByteBuffer) (bool, bool) { for _, child := range n.children { switch child.nType { case static: found, tsr := child.find(path, buf) if found { return found, tsr } case param: end := segmentEndIndex(path, false) if child.paramRegex != nil { end, _ = child.findEndIndexAndValues(path[:end]) if end == -1 { continue } } buf.WriteString(path[:end]) if len(path) > end { found, tsr := child.findFromChild(path[end:], buf) if found { return found, tsr } } else if len(path) == end { if child.tsr { buf.WriteByte('/') return true, true } if child.handler != nil { return true, false } } bufferRemoveString(buf, path[:end]) default: panic("invalid node type") } } if n.wildcard != nil { buf.WriteString(path) return true, false } return false, false } // sort sorts the current node and their children func (n *node) sort() { for _, child := range n.children { child.sort() } sort.Sort(n) } // Len returns the total number of children the node has func (n *node) Len() int { return len(n.children) } // Swap swaps the order of children nodes func (n *node) Swap(i, j int) { n.children[i], n.children[j] = n.children[j], n.children[i] } // Less checks if the node 'i' has less priority than the node 'j' func (n *node) Less(i, j int) bool { if n.children[i].nType < n.children[j].nType { return true } else if n.children[i].nType > n.children[j].nType { return false } return len(n.children[i].children) > len(n.children[j].children) } router-1.5.4/radix/node_test.go000066400000000000000000000447001473534360100164760ustar00rootroot00000000000000package radix import ( "fmt" "reflect" "strings" "testing" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) type testRequests []struct { path string nilHandler bool route string ps map[string]interface{} } type testRoute struct { path string conflict bool } // Used as a workaround since we can't compare functions or their addresses var fakeHandlerValue string func fakeHandler(val string) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { fakeHandlerValue = val } } func catchPanic(testFunc func()) (recv interface{}) { defer func() { recv = recover() }() testFunc() return } func acquireRequestCtx(path string) *fasthttp.RequestCtx { ctx := new(fasthttp.RequestCtx) req := new(fasthttp.Request) req.SetRequestURI(path) ctx.Init(req, nil, nil) return ctx } func checkRequests(t *testing.T, tree *Tree, requests testRequests) { for _, request := range requests { ctx := acquireRequestCtx(request.path) handler, _ := tree.Get(request.path, ctx) if handler == nil { if !request.nilHandler { t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) } } else if request.nilHandler { t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) } else { handler(ctx) if fakeHandlerValue != request.route { t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) } } params := make(map[string]interface{}) if request.ps == nil { request.ps = make(map[string]interface{}) } ctx.VisitUserValues(func(key []byte, value interface{}) { params[string(key)] = value }) if !reflect.DeepEqual(params, request.ps) { t.Errorf("Route %s - User values == %v, want %v", request.path, params, request.ps) } } } func testRoutes(t *testing.T, routes []testRoute) { tree := New() for _, route := range routes { recv := catchPanic(func() { tree.Add(route.path, fakeHandler(route.path)) }) if route.conflict { if recv == nil { t.Errorf("no panic for conflicting route '%s'", route.path) } } else if recv != nil { t.Errorf("unexpected panic for route '%s': %v", route.path, recv) } } } func TestTreeAddAndGet(t *testing.T) { tree := New() routes := [...]string{ "/hi", "/contact/", "/co", "/c", "/a", "/ab", "/doc/", "/doc/go_faq.html", "/doc/go1.html", "/α", "/β", "/hello/test", "/hello/tooth", "/hello/{name}", "/regex/{c1:big_alt|alt|small_alt}/{rest:*}", "/regex/{path:*}", } for _, route := range routes { tree.Add(route, fakeHandler(route)) } checkRequests(t, tree, testRequests{ {"/a", false, "/a", nil}, {"/", true, "", nil}, {"/hi", false, "/hi", nil}, {"/contact", true, "", nil}, // TSR {"/co", false, "/co", nil}, {"/con", true, "", nil}, // key mismatch {"/cona", true, "", nil}, // key mismatch {"/no", true, "", nil}, // no matching child {"/ab", false, "/ab", nil}, {"/α", false, "/α", nil}, {"/β", false, "/β", nil}, {"/hello/test", false, "/hello/test", nil}, {"/hello/tooth", false, "/hello/tooth", nil}, {"/hello/testastretta", false, "/hello/{name}", map[string]interface{}{"name": "testastretta"}}, {"/hello/tes", false, "/hello/{name}", map[string]interface{}{"name": "tes"}}, {"/hello/test/bye", true, "", nil}, {"/regex/more_alt/hello", false, "/regex/{path:*}", map[string]interface{}{"path": "more_alt/hello"}}, {"/regex/small_alt/hello", false, "/regex/{c1:big_alt|alt|small_alt}/{rest:*}", map[string]interface{}{"c1": "small_alt", "rest": "hello"}}, }) } func TestTreeWildcard(t *testing.T) { tree := New() routes := [...]string{ "/", "/cmd/{tool}/{sub}", "/cmd/{tool}/", "/src/{filepath:*}", "/src/data", "/search/", "/search/{query}", "/user_{name}", "/user_{name}/about", "/files/{dir}/{filepath:*}", "/doc/", "/doc/go_faq.html", "/doc/go1.html", "/info/{user}/public", "/info/{user}/project/{project}", } for _, route := range routes { tree.Add(route, fakeHandler(route)) } checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, {"/cmd/test/", false, "/cmd/{tool}/", map[string]interface{}{"tool": "test"}}, {"/cmd/test", true, "", nil}, {"/cmd/test/3", false, "/cmd/{tool}/{sub}", map[string]interface{}{"tool": "test", "sub": "3"}}, {"/src/", false, "/src/{filepath:*}", map[string]interface{}{"filepath": ""}}, {"/src/some/file.png", false, "/src/{filepath:*}", map[string]interface{}{"filepath": "some/file.png"}}, {"/search/", false, "/search/", nil}, {"/search/someth!ng+in+ünìcodé", false, "/search/{query}", map[string]interface{}{"query": "someth!ng+in+ünìcodé"}}, {"/search/someth!ng+in+ünìcodé/", true, "", nil}, {"/user_gopher", false, "/user_{name}", map[string]interface{}{"name": "gopher"}}, {"/user_gopher/about", false, "/user_{name}/about", map[string]interface{}{"name": "gopher"}}, {"/files/js/inc/framework.js", false, "/files/{dir}/{filepath:*}", map[string]interface{}{"dir": "js", "filepath": "inc/framework.js"}}, {"/info/gordon/public", false, "/info/{user}/public", map[string]interface{}{"user": "gordon"}}, {"/info/gordon/project/go", false, "/info/{user}/project/{project}", map[string]interface{}{"user": "gordon", "project": "go"}}, {"/info/gordon", true, "", nil}, }) } func TestTreeWildcardConflict(t *testing.T) { routes := []testRoute{ {"/cmd/{tool}/{sub}", false}, {"/cmd/vet", false}, {"/src/{filepath:*}", false}, {"/src/", false}, {"/src/{filepathx:*}", true}, {"/src1/", false}, {"/src1/{filepath:*}", false}, {"/src2{filepath:*}", true}, {"/search/{query}", false}, {"/search/invalid", false}, {"/user_{name}", false}, {"/user_x", false}, {"/user_{name}", true}, {"/id{id}", false}, {"/id/{id}", false}, } testRoutes(t, routes) } func TestTreeChildConflict(t *testing.T) { routes := []testRoute{ {"/cmd/vet", false}, {"/cmd/{tool}/{sub}", false}, {"/src/AUTHORS", false}, {"/src/{filepath:*}", false}, {"/user_x", false}, {"/user_{name}", false}, {"/id/{id}", false}, {"/id{id}", false}, {"/{users}", false}, {"/{id}/", true}, {"/{filepath:*}", false}, {"/asd{filepath:*}", true}, {"/abc", false}, {"/abd", false}, {"/abcc", false}, } testRoutes(t, routes) } func TestTreeDuplicatePath(t *testing.T) { tree := New() routes := [...]string{ "/", "/doc/", "/src/{filepath:*}", "/search/{query}", "/user_{name}", } for _, route := range routes { handler := fakeHandler(route) recv := catchPanic(func() { tree.Add(route, handler) }) if recv != nil { t.Fatalf("panic inserting route '%s': %v", route, recv) } // Add again recv = catchPanic(func() { tree.Add(route, handler) }) if recv == nil { t.Fatalf("no panic while inserting duplicate route '%s", route) } } checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, {"/doc/", false, "/doc/", nil}, {"/src/some/file.png", false, "/src/{filepath:*}", map[string]interface{}{"filepath": "some/file.png"}}, {"/search/someth!ng+in+ünìcodé", false, "/search/{query}", map[string]interface{}{"query": "someth!ng+in+ünìcodé"}}, {"/user_gopher", false, "/user_{name}", map[string]interface{}{"name": "gopher"}}, }) } func TestEmptyWildcardName(t *testing.T) { tree := New() routes := [...]string{ "/user{}", "/user{}/", "/cmd/{}/", "/src/{:*}", } for _, route := range routes { recv := catchPanic(func() { tree.Add(route, fakeHandler(route)) }) if recv == nil { t.Errorf("no panic while inserting route with empty wildcard name '%s", route) } } } func TestTreeCatchAllConflict(t *testing.T) { routes := []testRoute{ {"/src/{filepath:*}/x", true}, {"/src2/", false}, {"/src2/{filepath:*}/x", true}, {"/src3/{filepath:*}", false}, {"/src3/{filepath:*}/x", true}, } testRoutes(t, routes) } func TestTreeCatchAllConflictRoot(t *testing.T) { routes := []testRoute{ {"/", false}, {"/{filepath:*}", false}, } testRoutes(t, routes) } func TestTreeDoubleWildcard(t *testing.T) { const panicMsg = "the wildcards must be separated by at least 1 char" routes := [...]string{ "/{foo}{bar}", "/{foo}{bar}/", "/{foo}{bar:*}", } for _, route := range routes { tree := New() recv := catchPanic(func() { tree.Add(route, fakeHandler(route)) }) if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) } } } func TestTreeTrailingSlashRedirect(t *testing.T) { tree := New() routes := [...]string{ "/hi", "/b/", "/search/{query}", "/cmd/{tool}/", "/src/{filepath:*}", "/x", "/x/y", "/y/", "/y/z", "/0/{id}", "/0/{id}/1", "/1/{id}/", "/1/{id}/2", "/aa", "/a/", "/admin", "/admin/{category}", "/admin/{category}/{page}", "/doc", "/doc/go_faq.html", "/doc/go1.html", "/no/a", "/no/b", "/api/hello/{name}", "/foo/data/hello", "/foo/", } for _, route := range routes { recv := catchPanic(func() { tree.Add(route, fakeHandler(route)) }) if recv != nil { t.Fatalf("panic inserting route '%s': %v", route, recv) } } tsrRoutes := [...]string{ "/hi/", "/b", "/search/gopher/", "/cmd/vet", "/src", "/x/", "/y", "/0/go/", "/1/go", "/a", "/admin/", "/admin/config/", "/admin/config/permissions/", "/doc/", "/foo/data/hello/", "/foo", } for _, route := range tsrRoutes { handler, tsr := tree.Get(route, nil) if handler != nil { t.Fatalf("non-nil handler for TSR route '%s", route) } else if !tsr { t.Errorf("expected TSR recommendation for route '%s'", route) } } noTsrRoutes := [...]string{ "/", "/no", "/no/", "/_", "/_/", "/api/world/abc", } for _, route := range noTsrRoutes { handler, tsr := tree.Get(route, nil) if handler != nil { t.Fatalf("non-nil handler for No-TSR route '%s", route) } else if tsr { t.Errorf("expected no TSR recommendation for route '%s'", route) } } } func TestTreeRootTrailingSlashRedirect(t *testing.T) { tree := New() recv := catchPanic(func() { tree.Add("/{test}", fakeHandler("/{test}")) }) if recv != nil { t.Fatalf("panic inserting test route: %v", recv) } handler, tsr := tree.Get("/", nil) if handler != nil { t.Fatalf("non-nil handler") } else if tsr { t.Errorf("expected no TSR recommendation") } } func TestTreeFindCaseInsensitivePath(t *testing.T) { tree := New() longPath := "/l" + strings.Repeat("o", 128) + "ng" lOngPath := "/l" + strings.Repeat("O", 128) + "ng/" routes := [...]string{ "/hi", "/b/", "/ABC/", "/search/{query}", "/cmd/{tool}/", "/src/{filepath:*}", "/proc/{id}/status", "/regex/{id:.*}_test/data", "/x", "/x/y", "/y/", "/y/z", "/0/{id}", "/0/{id}/1", "/1/{id}/", "/1/{id}/2", "/aa", "/a/", "/doc", "/doc/go_faq.html", "/doc/go1.html", "/doc/go/away", "/no/a", "/no/b", "/Π", "/u/apfêl/", "/u/äpfêl/", "/u/äpkul/", "/u/öpfêl", "/v/Äpfêl/", "/v/Öpfêl", "/w/♬", // 3 byte "/w/♭/", // 3 byte, last byte differs "/w/𠜎", // 4 byte "/w/𠜏/", // 4 byte longPath, } for _, route := range routes { recv := catchPanic(func() { tree.Add(route, fakeHandler(route)) }) if recv != nil { t.Fatalf("panic inserting route '%s': %v", route, recv) } } buf := bytebufferpool.Get() // Check out == in for all registered routes // With fixTrailingSlash = true for _, route := range routes { found := tree.FindCaseInsensitivePath(route, true, buf) if !found { t.Errorf("Route '%s' not found!", route) } else if out := buf.String(); out != route { t.Errorf("Wrong result for route '%s': %s", route, out) } buf.Reset() } // With fixTrailingSlash = false for _, route := range routes { found := tree.FindCaseInsensitivePath(route, false, buf) if !found { t.Errorf("Route '%s' not found!", route) } else if out := buf.String(); out != route { t.Errorf("Wrong result for route '%s': %s", route, out) } buf.Reset() } tests := []struct { in string out string found bool slash bool }{ {"/HI", "/hi", true, false}, {"/HI/", "/hi", true, true}, {"/B", "/b/", true, true}, {"/B/", "/b/", true, false}, {"/abc", "/ABC/", true, true}, {"/abc/", "/ABC/", true, false}, {"/aBc", "/ABC/", true, true}, {"/aBc/", "/ABC/", true, false}, {"/abC", "/ABC/", true, true}, {"/abC/", "/ABC/", true, false}, {"/SEARCH/QUERY", "/search/QUERY", true, false}, {"/SEARCH/QUERY/", "/search/QUERY", true, true}, {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, {"/CMD/TOOL", "/cmd/TOOL/", true, true}, {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, {"/ProC/112", "", false, false}, {"/RegEx/a1b2_test/DaTA", "/regex/a1b2_test/data", true, false}, {"/RegEx/A1B2_test/DaTA/", "/regex/A1B2_test/data", true, true}, {"/RegEx/blabla/DaTA/", "", false, false}, {"/RegEx/blabla_test/fail", "", false, false}, {"/x/Y", "/x/y", true, false}, {"/x/Y/", "/x/y", true, true}, {"/X/y", "/x/y", true, false}, {"/X/y/", "/x/y", true, true}, {"/X/Y", "/x/y", true, false}, {"/X/Y/", "/x/y", true, true}, {"/Y/", "/y/", true, false}, {"/Y", "/y/", true, true}, {"/Y/z", "/y/z", true, false}, {"/Y/z/", "/y/z", true, true}, {"/Y/Z", "/y/z", true, false}, {"/Y/Z/", "/y/z", true, true}, {"/y/Z", "/y/z", true, false}, {"/y/Z/", "/y/z", true, true}, {"/Aa", "/aa", true, false}, {"/Aa/", "/aa", true, true}, {"/AA", "/aa", true, false}, {"/AA/", "/aa", true, true}, {"/aA", "/aa", true, false}, {"/aA/", "/aa", true, true}, {"/A/", "/a/", true, false}, {"/A", "/a/", true, true}, {"/DOC", "/doc", true, false}, {"/DOC/", "/doc", true, true}, {"/NO", "", false, true}, {"/DOC/GO", "", false, true}, {"/π", "/Π", true, false}, {"/π/", "/Π", true, true}, {"/u/ÄPFÊL/", "/u/äpfêl/", true, false}, {"/U/ÄPKUL/", "/u/äpkul/", true, false}, {"/u/ÄPFÊL", "/u/äpfêl/", true, true}, {"/u/ÖPFÊL/", "/u/öpfêl", true, true}, {"/u/ÖPFÊL", "/u/öpfêl", true, false}, {"/v/äpfêL/", "/v/Äpfêl/", true, false}, {"/v/äpfêL", "/v/Äpfêl/", true, true}, {"/v/öpfêL/", "/v/Öpfêl", true, true}, {"/v/öpfêL", "/v/Öpfêl", true, false}, {"/w/♬/", "/w/♬", true, true}, {"/w/♭", "/w/♭/", true, true}, {"/w/𠜎/", "/w/𠜎", true, true}, {"/w/𠜏", "/w/𠜏/", true, true}, {lOngPath, longPath, true, true}, } // With fixTrailingSlash = true for _, test := range tests { found := tree.FindCaseInsensitivePath(test.in, true, buf) if out := buf.String(); found != test.found || (found && (out != test.out)) { t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", test.in, string(out), found, test.out, test.found) } buf.Reset() } // With fixTrailingSlash = false for _, test := range tests { found := tree.FindCaseInsensitivePath(test.in, false, buf) if test.slash { if found { // test needs a trailingSlash fix. It must not be found! t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, buf.String()) } } else { if out := buf.String(); found != test.found || (found && (out != test.out)) { t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", test.in, out, found, test.out, test.found) } } buf.Reset() } } func TestTreeInvalidNodeType(t *testing.T) { const panicMsg = "invalid node type" tree := New() tree.Add("/", fakeHandler("/")) tree.Add("/{page}", fakeHandler("/{page}")) // set invalid node type tree.root.children[0].nType = 42 // normal lookup recv := catchPanic(func() { tree.Get("/test", nil) }) if rs, ok := recv.(string); !ok || rs != panicMsg { t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) } // case-insensitive lookup recv = catchPanic(func() { tree.FindCaseInsensitivePath("/test", true, bytebufferpool.Get()) }) if rs, ok := recv.(string); !ok || rs != panicMsg { t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) } } func TestTreeWildcardConflictEx(t *testing.T) { routes := [...]string{ "/con{tact}", "/who/are/{you:*}", "/who/foo/hello", "/whose/{users}/{name}", "/{filepath:*}", "/{id}", } conflicts := []struct { route string wantErr bool wantErrText string }{ {route: "/who/are/foo", wantErr: false}, {route: "/who/are/foo/", wantErr: false}, {route: "/who/are/foo/bar", wantErr: false}, {route: "/conxxx", wantErr: false}, {route: "/conooo/xxx", wantErr: false}, { route: "invalid/data", wantErr: true, wantErrText: "path must begin with '/' in path 'invalid/data'", }, { route: "/con{tact}", wantErr: true, wantErrText: "a handler is already registered for path '/con{tact}'", }, { route: "/con{something}", wantErr: true, wantErrText: "'{something}' in new path '/con{something}' conflicts with existing wild path '{tact}' in existing prefix '/con{tact}'", }, { route: "/who/are/{you:*}", wantErr: true, wantErrText: "a wildcard handler is already registered for path '/who/are/{you:*}'", }, { route: "/who/are/{me:*}", wantErr: true, wantErrText: "'{me:*}' in new path '/who/are/{me:*}' conflicts with existing wildcard '{you:*}' in existing prefix '/who/are/{you:*}'", }, { route: "/who/foo/hello", wantErr: true, wantErrText: "a handler is already registered for path '/who/foo/hello'", }, { route: "/{static:*}", wantErr: true, wantErrText: "'{static:*}' in new path '/{static:*}' conflicts with existing wildcard '{filepath:*}' in existing prefix '/{filepath:*}'", }, { route: "/static/{filepath:*}/other", wantErr: true, wantErrText: "wildcard routes are only allowed at the end of the path in path '/static/{filepath:*}/other'", }, { route: "/{user}/", wantErr: true, wantErrText: "'{user}' in new path '/{user}/' conflicts with existing wild path '{id}' in existing prefix '/{id}'", }, { route: "/prefix{filepath:*}", wantErr: true, wantErrText: "no / before wildcard in path '/prefix{filepath:*}'", }, } for _, conflict := range conflicts { // I have to re-create a 'tree', because the 'tree' will be // in an inconsistent state when the loop recovers from the // panic which threw by 'addRoute' function. tree := New() for _, route := range routes { tree.Add(route, fakeHandler(route)) } err := catchPanic(func() { tree.Add(conflict.route, fakeHandler(conflict.route)) }) if conflict.wantErr == (err == nil) { t.Errorf("Unexpected error: %v", err) } if err != nil && conflict.wantErrText != fmt.Sprint(err) { t.Errorf("Invalid conflict error text (%v)", err) } } } router-1.5.4/radix/tree.go000066400000000000000000000047511473534360100154530ustar00rootroot00000000000000package radix import ( "errors" "strings" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) // New returns an empty routes storage func New() *Tree { return &Tree{ root: &node{ nType: root, }, } } // Add adds a node with the given handle to the path. // // WARNING: Not concurrency-safe! func (t *Tree) Add(path string, handler fasthttp.RequestHandler) { if !strings.HasPrefix(path, "/") { panicf("path must begin with '/' in path '%s'", path) } else if handler == nil { panic("nil handler") } fullPath := path i := longestCommonPrefix(path, t.root.path) if i > 0 { if len(t.root.path) > i { t.root.split(i) } path = path[i:] } n, err := t.root.add(path, fullPath, handler) if err != nil { var radixErr radixError if errors.As(err, &radixErr) && t.Mutable && !n.tsr { switch radixErr.msg { case errSetHandler: n.handler = handler return case errSetWildcardHandler: n.wildcard.handler = handler return } } panic(err) } if len(t.root.path) == 0 { t.root = t.root.children[0] t.root.nType = root } // Reorder the nodes t.root.sort() } // Get returns the handle registered with the given path (key). The values of // param/wildcard are saved as ctx.UserValue. // 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 (t *Tree) Get(path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) { if len(path) > len(t.root.path) { if path[:len(t.root.path)] != t.root.path { return nil, false } path = path[len(t.root.path):] return t.root.getFromChild(path, ctx) } else if path == t.root.path { switch { case t.root.tsr: return nil, true case t.root.handler != nil: return t.root.handler, false case t.root.wildcard != nil: if ctx != nil { ctx.SetUserValue(t.root.wildcard.paramKey, "") } return t.root.wildcard.handler, false } } return nil, false } // FindCaseInsensitivePath 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 (t *Tree) FindCaseInsensitivePath(path string, fixTrailingSlash bool, buf *bytebufferpool.ByteBuffer) bool { found, tsr := t.root.find(path, buf) if !found || (tsr && !fixTrailingSlash) { buf.Reset() return false } return true } router-1.5.4/radix/tree_test.go000066400000000000000000000200101473534360100164740ustar00rootroot00000000000000package radix import ( "fmt" "reflect" "testing" "github.com/savsgio/gotils/bytes" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) func generateHandler() fasthttp.RequestHandler { hex := bytes.Rand(make([]byte, 10)) return func(ctx *fasthttp.RequestCtx) { ctx.Write(hex) } } func testHandlerAndParams( t *testing.T, tree *Tree, reqPath string, handler fasthttp.RequestHandler, wantTSR bool, params map[string]interface{}, ) { for _, ctx := range []*fasthttp.RequestCtx{new(fasthttp.RequestCtx), nil} { h, tsr := tree.Get(reqPath, ctx) if reflect.ValueOf(handler).Pointer() != reflect.ValueOf(h).Pointer() { t.Errorf("Path '%s' handler == %p, want %p", reqPath, h, handler) } if wantTSR != tsr { t.Errorf("Path '%s' tsr == %v, want %v", reqPath, tsr, wantTSR) } if ctx != nil { resultParams := make(map[string]interface{}) if params == nil { params = make(map[string]interface{}) } ctx.VisitUserValues(func(key []byte, value interface{}) { resultParams[string(key)] = value }) if !reflect.DeepEqual(resultParams, params) { t.Errorf("Path '%s' User values == %v, want %v", reqPath, resultParams, params) } } } } func Test_Tree(t *testing.T) { type args struct { path string reqPath string handler fasthttp.RequestHandler } type want struct { tsr bool params map[string]interface{} } tests := []struct { args args want want }{ { args: args{ path: "/users/{name}", reqPath: "/users/atreugo", handler: generateHandler(), }, want: want{ params: map[string]interface{}{ "name": "atreugo", }, }, }, { args: args{ path: "/users", reqPath: "/users", handler: generateHandler(), }, want: want{ params: nil, }, }, { args: args{ path: "/user/", reqPath: "/user", handler: generateHandler(), }, want: want{ tsr: true, params: nil, }, }, { args: args{ path: "/", reqPath: "/", handler: generateHandler(), }, want: want{ params: nil, }, }, { args: args{ path: "/users/{name}/jobs", reqPath: "/users/atreugo/jobs", handler: generateHandler(), }, want: want{ params: map[string]interface{}{ "name": "atreugo", }, }, }, { args: args{ path: "/users/admin", reqPath: "/users/admin", handler: generateHandler(), }, want: want{ params: nil, }, }, { args: args{ path: "/users/{status}/proc", reqPath: "/users/active/proc", handler: generateHandler(), }, want: want{ params: map[string]interface{}{ "status": "active", }, }, }, { args: args{ path: "/static/{filepath:*}", reqPath: "/static/assets/js/main.js", handler: generateHandler(), }, want: want{ params: map[string]interface{}{ "filepath": "assets/js/main.js", }, }, }, { args: args{ path: "/data/orders", reqPath: "/data/orders/", handler: generateHandler(), }, want: want{ tsr: true, params: nil, }, }, { args: args{ path: "/data/", reqPath: "/data", handler: generateHandler(), }, want: want{ tsr: true, params: nil, }, }, } tree := New() for _, test := range tests { tree.Add(test.args.path, test.args.handler) } for _, test := range tests { wantHandler := test.args.handler if test.want.tsr { wantHandler = nil } testHandlerAndParams(t, tree, test.args.reqPath, wantHandler, test.want.tsr, test.want.params) } filepathHandler := generateHandler() tree.Add("/{filepath:*}", filepathHandler) testHandlerAndParams(t, tree, "/js/main.js", filepathHandler, false, map[string]interface{}{ "filepath": "js/main.js", }) } func Test_Get(t *testing.T) { handler := generateHandler() tree := New() tree.Add("/api/", handler) testHandlerAndParams(t, tree, "/api", nil, true, nil) testHandlerAndParams(t, tree, "/api/", handler, false, nil) testHandlerAndParams(t, tree, "/notfound", nil, false, nil) tree = New() tree.Add("/api", handler) testHandlerAndParams(t, tree, "/api", handler, false, nil) testHandlerAndParams(t, tree, "/api/", nil, true, nil) testHandlerAndParams(t, tree, "/notfound", nil, false, nil) } func Test_AddWithParam(t *testing.T) { handler := generateHandler() tree := New() tree.Add("/test", handler) tree.Add("/api/prefix{version:V[0-9]}_{name:[a-z]+}_sufix/files", handler) tree.Add("/api/prefix{version:V[0-9]}_{name:[a-z]+}_sufix/data", handler) tree.Add("/api/prefix/files", handler) tree.Add("/prefix{name:[a-z]+}suffix/data", handler) tree.Add("/prefix{name:[a-z]+}/data", handler) tree.Add("/api/{file}.json", handler) testHandlerAndParams(t, tree, "/api/prefixV1_atreugo_sufix/files", handler, false, map[string]interface{}{ "version": "V1", "name": "atreugo", }) testHandlerAndParams(t, tree, "/api/prefixV1_atreugo_sufix/data", handler, false, map[string]interface{}{ "version": "V1", "name": "atreugo", }) testHandlerAndParams(t, tree, "/prefixatreugosuffix/data", handler, false, map[string]interface{}{ "name": "atreugo", }) testHandlerAndParams(t, tree, "/prefixatreugo/data", handler, false, map[string]interface{}{ "name": "atreugo", }) testHandlerAndParams(t, tree, "/api/name.json", handler, false, map[string]interface{}{ "file": "name", }) // Not found testHandlerAndParams(t, tree, "/api/prefixV1_1111_sufix/fake", nil, false, nil) } func Test_TreeRootWildcard(t *testing.T) { handler := generateHandler() tree := New() tree.Add("/{filepath:*}", handler) testHandlerAndParams(t, tree, "/", handler, false, map[string]interface{}{ "filepath": "", }) tree.Add("/hello/{a}/{b}/{c}", handler) testHandlerAndParams(t, tree, "/hello/a", handler, false, map[string]interface{}{ "filepath": "hello/a", }) } func Test_TreeNilHandler(t *testing.T) { const panicMsg = "nil handler" tree := New() err := catchPanic(func() { tree.Add("/", nil) }) if err == nil { t.Fatal("Expected panic") } if err != nil && panicMsg != fmt.Sprint(err) { t.Errorf("Invalid conflict error text (%v)", err) } } func Test_TreeMutable(t *testing.T) { routes := []string{ "/", "/api/{version}", "/{filepath:*}", "/user{user:a-Z+}", } handler := generateHandler() tree := New() for _, route := range routes { tree.Add(route, handler) err := catchPanic(func() { tree.Add(route, handler) }) if err == nil { t.Errorf("Route '%s' - Expected panic", route) } } tree.Mutable = true for _, route := range routes { err := catchPanic(func() { tree.Add(route, handler) }) if err != nil { t.Errorf("Route '%s' - Unexpected panic: %v", route, err) } } } func Benchmark_Get(b *testing.B) { handler := func(ctx *fasthttp.RequestCtx) {} tree := New() // for i := 0; i < 3000; i++ { // tree.Add( // fmt.Sprintf("/%s", bytes.Rand(make([]byte, 15))), handler, // ) // } tree.Add("/", handler) tree.Add("/plaintext", handler) tree.Add("/json", handler) tree.Add("/fortune", handler) tree.Add("/fortune-quick", handler) tree.Add("/db", handler) tree.Add("/queries", handler) tree.Add("/update", handler) ctx := new(fasthttp.RequestCtx) b.ResetTimer() for i := 0; i < b.N; i++ { tree.Get("/update", ctx) } } func Benchmark_GetWithRegex(b *testing.B) { handler := func(ctx *fasthttp.RequestCtx) {} tree := New() ctx := new(fasthttp.RequestCtx) tree.Add("/api/{version:v[0-9]}/data", handler) b.ResetTimer() for i := 0; i < b.N; i++ { tree.Get("/api/v1/data", ctx) } } func Benchmark_GetWithParams(b *testing.B) { handler := func(ctx *fasthttp.RequestCtx) {} tree := New() ctx := new(fasthttp.RequestCtx) tree.Add("/api/{version}/data", handler) b.ResetTimer() for i := 0; i < b.N; i++ { tree.Get("/api/v1/data", ctx) } } func Benchmark_FindCaseInsensitivePath(b *testing.B) { handler := func(ctx *fasthttp.RequestCtx) {} tree := New() buf := bytebufferpool.Get() tree.Add("/endpoint", handler) b.ResetTimer() for i := 0; i < b.N; i++ { tree.FindCaseInsensitivePath("/ENdpOiNT", false, buf) buf.Reset() } } router-1.5.4/radix/types.go000066400000000000000000000012541473534360100156530ustar00rootroot00000000000000package radix import ( "regexp" "github.com/valyala/fasthttp" ) type nodeType uint8 type nodeWildcard struct { path string paramKey string handler fasthttp.RequestHandler } type node struct { nType nodeType path string tsr bool handler fasthttp.RequestHandler hasWildChild bool children []*node wildcard *nodeWildcard paramKeys []string paramRegex *regexp.Regexp } type wildPath struct { path string keys []string start int end int pType nodeType pattern string regex *regexp.Regexp } // Tree is a routes storage type Tree struct { root *node // If enabled, the node handler could be updated Mutable bool } router-1.5.4/radix/utils.go000066400000000000000000000066511473534360100156550ustar00rootroot00000000000000package radix import ( "fmt" "regexp" "strings" "unicode/utf8" "github.com/valyala/bytebufferpool" ) func panicf(s string, args ...interface{}) { panic(fmt.Sprintf(s, args...)) } func min(a, b int) int { if a <= b { return a } return b } func bufferRemoveString(buf *bytebufferpool.ByteBuffer, s string) { buf.B = buf.B[:len(buf.B)-len(s)] } // func isIndexEqual(a, b string) bool { // ra, _ := utf8.DecodeRuneInString(a) // rb, _ := utf8.DecodeRuneInString(b) // return unicode.ToLower(ra) == unicode.ToLower(rb) // } // longestCommonPrefix finds the longest common prefix. // This also implies that the common prefix contains no ':' or '*' // since the existing key can't contain those chars. func longestCommonPrefix(a, b string) int { i := 0 max := min(utf8.RuneCountInString(a), utf8.RuneCountInString(b)) for i < max { ra, sizeA := utf8.DecodeRuneInString(a) rb, sizeB := utf8.DecodeRuneInString(b) a = a[sizeA:] b = b[sizeB:] if ra != rb { return i } i += sizeA } return i } // segmentEndIndex returns the index where the segment ends from the given path func segmentEndIndex(path string, includeTSR bool) int { end := 0 for end < len(path) && path[end] != '/' { end++ } if includeTSR && path[end:] == "/" { end++ } return end } // findWildPath search for a wild path segment and check the name for invalid characters. // Returns -1 as index, if no param/wildcard was found. func findWildPath(path string, fullPath string) *wildPath { // Find start for start, c := range []byte(path) { // A wildcard starts with ':' (param) or '*' (wildcard) if c != '{' { continue } withRegex := false keys := 0 // Find end and check for invalid characters for end, c := range []byte(path[start+1:]) { switch c { case '}': if keys > 0 { keys-- continue } end := start + end + 2 wp := &wildPath{ path: path[start:end], keys: []string{path[start+1 : end-1]}, start: start, end: end, pType: param, } if len(path) > end && path[end] == '{' { panic("the wildcards must be separated by at least 1 char") } sn := strings.SplitN(wp.keys[0], ":", 2) if len(sn) > 1 { wp.keys = []string{sn[0]} pattern := sn[1] if pattern == "*" { wp.pattern = pattern wp.pType = wildcard } else { wp.pattern = "(" + pattern + ")" wp.regex = regexp.MustCompile(wp.pattern) } } else if path[len(path)-1] != '/' { wp.pattern = "(.*)" } if len(wp.keys[0]) == 0 { panicf("wildcards must be named with a non-empty name in path '%s'", fullPath) } segEnd := end + segmentEndIndex(path[end:], true) path = path[end:segEnd] if path == "/" { // Last segment, so include the TSR path = "" wp.end++ } if len(path) > 0 { // Rebuild the wildpath with the prefix wp2 := findWildPath(path, fullPath) if wp2 != nil { prefix := path[:wp2.start] wp.end += wp2.end wp.path += prefix + wp2.path wp.pattern += prefix + wp2.pattern wp.keys = append(wp.keys, wp2.keys...) } else { wp.path += path wp.pattern += path wp.end += len(path) } wp.regex = regexp.MustCompile(wp.pattern) } return wp case ':': withRegex = true case '{': if !withRegex && keys == 0 { panic("the char '{' is not allowed in the param name") } keys++ } } } return nil } router-1.5.4/radix/utils_test.go000066400000000000000000000104271473534360100167100ustar00rootroot00000000000000package radix import ( "fmt" "reflect" "regexp" "testing" ) func Test_findWildPath(t *testing.T) { type test struct { path string want wildPath } tests := []test{ { path: "/api/{param1}/data", want: wildPath{ path: "{param1}", keys: []string{"param1"}, start: 5, end: 13, pType: param, regex: nil, }, }, { path: "/api/{param1}_{param2}/data", want: wildPath{ path: "{param1}_{param2}", keys: []string{"param1", "param2"}, start: 5, end: 22, pType: param, regex: regexp.MustCompile("(.*)_(.*)"), }, }, { path: "/api/{param1:[a-z]{2}}/data", want: wildPath{ path: "{param1:[a-z]{2}}", keys: []string{"param1"}, start: 5, end: 22, pType: param, regex: regexp.MustCompile("([a-z]{2})"), }, }, { path: "/api/{param1:[a-z]{1}:[0-9]{1}}/data", want: wildPath{ path: "{param1:[a-z]{1}:[0-9]{1}}", keys: []string{"param1"}, start: 5, end: 31, pType: param, regex: regexp.MustCompile("([a-z]{1}:[0-9]{1})"), }, }, { path: "/api/{param1:[a-z]{3}}_{param2}/data", want: wildPath{ path: "{param1:[a-z]{3}}_{param2}", keys: []string{"param1", "param2"}, start: 5, end: 31, pType: param, regex: regexp.MustCompile("([a-z]{3})_(.*)"), }, }, { path: "/api/prefix{param1:[a-z]{3}}_{param2}suffix/data", want: wildPath{ path: "{param1:[a-z]{3}}_{param2}suffix", keys: []string{"param1", "param2"}, start: 11, end: 43, pType: param, regex: regexp.MustCompile("([a-z]{3})_(.*)suffix"), }, }, } for _, test := range tests { fullPath := test.path result := findWildPath(test.path, fullPath) if result.path != test.want.path { t.Errorf("wildPath.path == %s, want %s", result.path, test.want.path) } if !reflect.DeepEqual(result.keys, test.want.keys) { t.Errorf("wildPath.key == %v, want %v", result.keys, test.want.keys) } if result.start != test.want.start { t.Errorf("wildPath.start == %d, want %d", result.start, test.want.start) } if result.end != test.want.end { t.Errorf("wildPath.end == %d, want %d", result.end, test.want.end) } resultHasRegex := result.regex != nil wantHasRegex := test.want.regex != nil if resultHasRegex && wantHasRegex { resultRegex := result.regex.String() wantRegex := test.want.regex.String() if resultRegex != wantRegex { t.Errorf("wildPath.regex == %s, want %s", resultRegex, wantRegex) } } else if resultHasRegex != wantHasRegex { t.Errorf("wildPath.regex == %v, want %v", result.regex, test.want.regex) } } } func Test_findWildPathConflict(t *testing.T) { type test struct { path string wantErr bool wantErrText string } tests := []test{ { path: "/api/{version}{fake}/data", wantErr: true, wantErrText: "the wildcards must be separated by at least 1 char", }, { path: "/api/{}/data", wantErr: true, wantErrText: "wildcards must be named with a non-empty name in path '/api/{}/data'", }, { path: "/api/{version{bad}}/data", wantErr: true, wantErrText: "the char '{' is not allowed in the param name", }, { path: "/api/{version{bad}:^[a-z]{2}}/data", wantErr: true, wantErrText: "the char '{' is not allowed in the param name", }, { path: "/api/{version:^[a-z]{2}}/data", wantErr: false, }, { path: "/api/{version:^[a-z]{2}}{bad}/data", wantErr: true, wantErrText: "the wildcards must be separated by at least 1 char", }, { path: "/api/{version{bad1}:^[a-z]{2}:123}{bad2}/data", wantErr: true, wantErrText: "the char '{' is not allowed in the param name", }, { path: "/api/{version:^[a-z]{2}:123}/data", wantErr: false, wantErrText: "", }, { path: "/api/{param1:^[a-z]{3}}_{param2}/data", wantErr: false, wantErrText: "", }, } for _, test := range tests { fullPath := test.path err := catchPanic(func() { findWildPath(test.path, fullPath) }) if test.wantErr != (err != nil) { t.Errorf("Unexpected panic for path '%s': %v", test.path, err) } if err != nil && test.wantErrText != fmt.Sprint(err) { t.Errorf("Invalid conflict error text for path '%s': %v", test.path, err) } } } router-1.5.4/router.go000066400000000000000000000326601473534360100147250ustar00rootroot00000000000000package router import ( "fmt" "io/fs" "strings" "github.com/fasthttp/router/radix" "github.com/savsgio/gotils/bytes" "github.com/savsgio/gotils/strconv" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) // MethodWild wild HTTP method const MethodWild = "*" var ( questionMark = byte('?') // MatchedRoutePathParam is the param name under which the path of the matched // route is stored, if Router.SaveMatchedRoutePath is set. MatchedRoutePathParam = fmt.Sprintf("__matchedRoutePath::%s__", bytes.Rand(make([]byte, 15))) ) // New returns a new router. // Path auto-correction, including trailing slashes, is enabled by default. func New() *Router { return &Router{ trees: make([]*radix.Tree, 10), customMethodsIndex: make(map[string]int), registeredPaths: make(map[string][]string), RedirectTrailingSlash: true, RedirectFixedPath: true, HandleMethodNotAllowed: true, HandleOPTIONS: true, } } // Group returns a new group. // Path auto-correction, including trailing slashes, is enabled by default. func (r *Router) Group(path string) *Group { validatePath(path) if path != "/" && strings.HasSuffix(path, "/") { panic("group path must not end with a trailing slash") } return &Group{ router: r, prefix: path, } } func (r *Router) saveMatchedRoutePath(path string, handler fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { ctx.SetUserValue(MatchedRoutePathParam, path) handler(ctx) } } func (r *Router) methodIndexOf(method string) int { switch method { case fasthttp.MethodGet: return 0 case fasthttp.MethodHead: return 1 case fasthttp.MethodPost: return 2 case fasthttp.MethodPut: return 3 case fasthttp.MethodPatch: return 4 case fasthttp.MethodDelete: return 5 case fasthttp.MethodConnect: return 6 case fasthttp.MethodOptions: return 7 case fasthttp.MethodTrace: return 8 case MethodWild: return 9 } if i, ok := r.customMethodsIndex[method]; ok { return i } return -1 } // Mutable allows updating the route handler // // # It's disabled by default // // WARNING: Use with care. It could generate unexpected behaviours func (r *Router) Mutable(v bool) { r.treeMutable = v for i := range r.trees { tree := r.trees[i] if tree != nil { tree.Mutable = v } } } // List returns all registered routes grouped by method func (r *Router) List() map[string][]string { return r.registeredPaths } // GET is a shortcut for router.Handle(fasthttp.MethodGet, path, handler) func (r *Router) GET(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodGet, path, handler) } // HEAD is a shortcut for router.Handle(fasthttp.MethodHead, path, handler) func (r *Router) HEAD(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodHead, path, handler) } // POST is a shortcut for router.Handle(fasthttp.MethodPost, path, handler) func (r *Router) POST(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodPost, path, handler) } // PUT is a shortcut for router.Handle(fasthttp.MethodPut, path, handler) func (r *Router) PUT(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodPut, path, handler) } // PATCH is a shortcut for router.Handle(fasthttp.MethodPatch, path, handler) func (r *Router) PATCH(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodPatch, path, handler) } // DELETE is a shortcut for router.Handle(fasthttp.MethodDelete, path, handler) func (r *Router) DELETE(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodDelete, path, handler) } // CONNECT is a shortcut for router.Handle(fasthttp.MethodConnect, path, handler) func (r *Router) CONNECT(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodConnect, path, handler) } // OPTIONS is a shortcut for router.Handle(fasthttp.MethodOptions, path, handler) func (r *Router) OPTIONS(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodOptions, path, handler) } // TRACE is a shortcut for router.Handle(fasthttp.MethodTrace, path, handler) func (r *Router) TRACE(path string, handler fasthttp.RequestHandler) { r.Handle(fasthttp.MethodTrace, path, handler) } // ANY is a shortcut for router.Handle(router.MethodWild, path, handler) // // WARNING: Use only for routes where the request method is not important func (r *Router) ANY(path string, handler fasthttp.RequestHandler) { r.Handle(MethodWild, path, handler) } // ServeFiles serves files from the given file system root path. // 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 fasthttp.FSHandler is used, therefore fasthttp.NotFound is used instead // Use: // // router.ServeFiles("/src/{filepath:*}", "./") func (r *Router) ServeFiles(path string, rootPath string) { r.ServeFilesCustom(path, &fasthttp.FS{ Root: rootPath, IndexNames: []string{"index.html"}, GenerateIndexPages: true, AcceptByteRange: true, }) } // ServeFS serves files from the given file system. // 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 fasthttp.FSHandler is used, therefore fasthttp.NotFound is used instead // Use: // // router.ServeFS("/src/{filepath:*}", myFilesystem) func (r *Router) ServeFS(path string, filesystem fs.FS) { r.ServeFilesCustom(path, &fasthttp.FS{ FS: filesystem, Root: "", AllowEmptyRoot: true, GenerateIndexPages: true, AcceptByteRange: true, Compress: true, CompressBrotli: true, }) } // 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 fasthttp.FSHandler is used, therefore http.NotFound is used instead // of the Router's NotFound handler. // Use: // // router.ServeFilesCustom("/src/{filepath:*}", *customFS) func (r *Router) ServeFilesCustom(path string, fs *fasthttp.FS) { const suffix = "/{filepath:*}" if !strings.HasSuffix(path, suffix) { panic("path must end with " + suffix + " in path '" + path + "'") } prefix := path[:len(path)-len(suffix)] stripSlashes := strings.Count(prefix, "/") if fs.PathRewrite == nil && stripSlashes > 0 { fs.PathRewrite = fasthttp.NewPathSlashesStripper(stripSlashes) } fileHandler := fs.NewRequestHandler() r.GET(path, fileHandler) } // Handle registers a new request handler 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, handler fasthttp.RequestHandler) { switch { case len(method) == 0: panic("method must not be empty") case handler == nil: panic("handler must not be nil") default: validatePath(path) } r.registeredPaths[method] = append(r.registeredPaths[method], path) methodIndex := r.methodIndexOf(method) if methodIndex == -1 { tree := radix.New() tree.Mutable = r.treeMutable r.trees = append(r.trees, tree) methodIndex = len(r.trees) - 1 r.customMethodsIndex[method] = methodIndex } tree := r.trees[methodIndex] if tree == nil { tree = radix.New() tree.Mutable = r.treeMutable r.trees[methodIndex] = tree r.globalAllowed = r.allowed("*", "") } if r.SaveMatchedRoutePath { handler = r.saveMatchedRoutePath(path, handler) } optionalPaths := getOptionalPaths(path) // if not has optional paths, adds the original if len(optionalPaths) == 0 { tree.Add(path, handler) } else { for _, p := range optionalPaths { tree.Add(p, handler) } } } // 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 handler function. // Otherwise the second 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) { methodIndex := r.methodIndexOf(method) if methodIndex == -1 { return nil, false } if tree := r.trees[methodIndex]; tree != nil { handler, tsr := tree.Get(path, ctx) if handler != nil || tsr { return handler, tsr } } if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil { return tree.Get(path, ctx) } return nil, false } func (r *Router) recv(ctx *fasthttp.RequestCtx) { if rcv := recover(); rcv != nil { r.PanicHandler(ctx, rcv) } } func (r *Router) allowed(path, reqMethod string) (allow string) { allowed := make([]string, 0, 9) if path == "*" || path == "/*" { // server-wide{ // server-wide // empty method is used for internal calls to refresh the cache if reqMethod == "" { for method := range r.registeredPaths { if method == fasthttp.MethodOptions { continue } // Add request method to list of allowed methods allowed = append(allowed, method) } } else { return r.globalAllowed } } else { // specific path for method := range r.registeredPaths { // Skip the requested method - we already tried this one if method == reqMethod || method == fasthttp.MethodOptions { continue } handle, _ := r.trees[r.methodIndexOf(method)].Get(path, nil) if handle != nil { // Add request method to list of allowed methods allowed = append(allowed, method) } } } if len(allowed) > 0 { // Add request method to list of allowed methods allowed = append(allowed, fasthttp.MethodOptions) // Sort allowed methods. // sort.Strings(allowed) unfortunately causes unnecessary allocations // due to allowed being moved to the heap and interface conversion for i, l := 1, len(allowed); i < l; i++ { for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { allowed[j], allowed[j-1] = allowed[j-1], allowed[j] } } // return as comma separated list return strings.Join(allowed, ", ") } return } func (r *Router) tryRedirect(ctx *fasthttp.RequestCtx, tree *radix.Tree, tsr bool, method, path string) bool { // Moved Permanently, request with GET method code := fasthttp.StatusMovedPermanently if method != fasthttp.MethodGet { // Permanent Redirect, request with same method code = fasthttp.StatusPermanentRedirect } 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.WriteByte('/') } if queryBuf := ctx.URI().QueryString(); len(queryBuf) > 0 { uri.WriteByte(questionMark) uri.Write(queryBuf) } ctx.Redirect(uri.String(), code) bytebufferpool.Put(uri) return true } // Try to fix the request path if r.RedirectFixedPath { path2 := strconv.B2S(ctx.Request.URI().Path()) uri := bytebufferpool.Get() found := tree.FindCaseInsensitivePath( cleanPath(path2), r.RedirectTrailingSlash, uri, ) if found { if queryBuf := ctx.URI().QueryString(); len(queryBuf) > 0 { uri.WriteByte(questionMark) uri.Write(queryBuf) } ctx.Redirect(uri.String(), code) bytebufferpool.Put(uri) return true } bytebufferpool.Put(uri) } return false } // Handler makes the router implement the http.Handler interface. func (r *Router) Handler(ctx *fasthttp.RequestCtx) { if r.PanicHandler != nil { defer r.recv(ctx) } path := strconv.B2S(ctx.Request.URI().PathOriginal()) method := strconv.B2S(ctx.Request.Header.Method()) methodIndex := r.methodIndexOf(method) if methodIndex > -1 { if tree := r.trees[methodIndex]; tree != nil { if handler, tsr := tree.Get(path, ctx); handler != nil { handler(ctx) return } else if method != fasthttp.MethodConnect && path != "/" { if ok := r.tryRedirect(ctx, tree, tsr, method, path); ok { return } } } } // Try to search in the wild method tree if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil { if handler, tsr := tree.Get(path, ctx); handler != nil { handler(ctx) return } else if method != fasthttp.MethodConnect && path != "/" { if ok := r.tryRedirect(ctx, tree, tsr, method, path); ok { return } } } if r.HandleOPTIONS && method == fasthttp.MethodOptions { // Handle OPTIONS requests if allow := r.allowed(path, fasthttp.MethodOptions); allow != "" { ctx.Response.Header.Set("Allow", allow) if r.GlobalOPTIONS != nil { r.GlobalOPTIONS(ctx) } return } } else if r.HandleMethodNotAllowed { // Handle 405 if allow := r.allowed(path, method); allow != "" { ctx.Response.Header.Set("Allow", allow) if r.MethodNotAllowed != nil { r.MethodNotAllowed(ctx) } else { ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) 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) } } router-1.5.4/router_test.go000066400000000000000000000755461473534360100157760ustar00rootroot00000000000000package router import ( "bufio" "bytes" "embed" "fmt" "io/ioutil" "math/rand" "net" "os" "reflect" "runtime" "strings" "testing" "time" "github.com/valyala/fasthttp" ) type readWriter struct { net.Conn r bytes.Buffer w bytes.Buffer } var httpMethods = []string{ fasthttp.MethodGet, fasthttp.MethodHead, fasthttp.MethodPost, fasthttp.MethodPut, fasthttp.MethodPatch, fasthttp.MethodDelete, fasthttp.MethodConnect, fasthttp.MethodOptions, fasthttp.MethodTrace, MethodWild, "CUSTOM", } //go:embed LICENSE var fsTestFilesystem embed.FS func randomHTTPMethod() string { method := httpMethods[rand.Intn(len(httpMethods)-1)] for method == MethodWild { method = httpMethods[rand.Intn(len(httpMethods)-1)] } return method } func buildLocation(host, path string) string { return fmt.Sprintf("http://%s%s", host, path) } var zeroTCPAddr = &net.TCPAddr{ IP: net.IPv4zero, } func (rw *readWriter) Close() error { return nil } func (rw *readWriter) Read(b []byte) (int, error) { return rw.r.Read(b) } func (rw *readWriter) Write(b []byte) (int, error) { return rw.w.Write(b) } func (rw *readWriter) RemoteAddr() net.Addr { return zeroTCPAddr } func (rw *readWriter) LocalAddr() net.Addr { return zeroTCPAddr } func (rw *readWriter) SetReadDeadline(t time.Time) error { return nil } func (rw *readWriter) SetWriteDeadline(t time.Time) error { return nil } type assertFn func(rw *readWriter) func assertWithTestServer(t *testing.T, uri string, handler fasthttp.RequestHandler, fn assertFn) { s := &fasthttp.Server{ Handler: handler, } rw := &readWriter{} ch := make(chan error) rw.r.WriteString(uri) go func() { ch <- s.ServeConn(rw) }() select { case err := <-ch: if err != nil { t.Fatalf("return error %s", err) } case <-time.After(500 * time.Millisecond): t.Fatalf("timeout") } fn(rw) } func catchPanic(testFunc func()) (recv interface{}) { defer func() { recv = recover() }() testFunc() return } func TestRouter(t *testing.T) { router := New() routed := false router.Handle(fasthttp.MethodGet, "/user/{name}", func(ctx *fasthttp.RequestCtx) { routed = true want := "gopher" param, ok := ctx.UserValue("name").(string) if !ok { t.Fatalf("wrong wildcard values: param value is nil") } if param != want { t.Fatalf("wrong wildcard values: want %s, got %s", want, param) } }) ctx := new(fasthttp.RequestCtx) ctx.Request.SetRequestURI("/user/gopher") router.Handler(ctx) if !routed { t.Fatal("routing failed") } } func TestRouterAPI(t *testing.T) { var handled, get, head, post, put, patch, delete, connect, options, trace, any bool httpHandler := func(ctx *fasthttp.RequestCtx) { handled = true } router := New() router.GET("/GET", func(ctx *fasthttp.RequestCtx) { get = true }) router.HEAD("/HEAD", func(ctx *fasthttp.RequestCtx) { head = true }) router.POST("/POST", func(ctx *fasthttp.RequestCtx) { post = true }) router.PUT("/PUT", func(ctx *fasthttp.RequestCtx) { put = true }) router.PATCH("/PATCH", func(ctx *fasthttp.RequestCtx) { patch = true }) router.DELETE("/DELETE", func(ctx *fasthttp.RequestCtx) { delete = true }) router.CONNECT("/CONNECT", func(ctx *fasthttp.RequestCtx) { connect = true }) router.OPTIONS("/OPTIONS", func(ctx *fasthttp.RequestCtx) { options = true }) router.TRACE("/TRACE", func(ctx *fasthttp.RequestCtx) { trace = true }) router.ANY("/ANY", func(ctx *fasthttp.RequestCtx) { any = true }) router.Handle(fasthttp.MethodGet, "/Handler", httpHandler) ctx := new(fasthttp.RequestCtx) var request = func(method, path string) { ctx.Request.Header.SetMethod(method) ctx.Request.SetRequestURI(path) router.Handler(ctx) } request(fasthttp.MethodGet, "/GET") if !get { t.Error("routing GET failed") } request(fasthttp.MethodHead, "/HEAD") if !head { t.Error("routing HEAD failed") } request(fasthttp.MethodPost, "/POST") if !post { t.Error("routing POST failed") } request(fasthttp.MethodPut, "/PUT") if !put { t.Error("routing PUT failed") } request(fasthttp.MethodPatch, "/PATCH") if !patch { t.Error("routing PATCH failed") } request(fasthttp.MethodDelete, "/DELETE") if !delete { t.Error("routing DELETE failed") } request(fasthttp.MethodConnect, "/CONNECT") if !connect { t.Error("routing CONNECT failed") } request(fasthttp.MethodOptions, "/OPTIONS") if !options { t.Error("routing OPTIONS failed") } request(fasthttp.MethodTrace, "/TRACE") if !trace { t.Error("routing TRACE failed") } request(fasthttp.MethodGet, "/Handler") if !handled { t.Error("routing Handler failed") } for _, method := range httpMethods { request(method, "/ANY") if !any { t.Errorf("routing ANY failed - Method: %s", method) } any = false } } func TestRouterInvalidInput(t *testing.T) { router := New() handle := func(_ *fasthttp.RequestCtx) {} recv := catchPanic(func() { router.Handle("", "/", handle) }) if recv == nil { t.Fatal("registering empty method did not panic") } recv = catchPanic(func() { router.GET("", handle) }) if recv == nil { t.Fatal("registering empty path did not panic") } recv = catchPanic(func() { router.GET("noSlashRoot", handle) }) if recv == nil { t.Fatal("registering path not beginning with '/' did not panic") } recv = catchPanic(func() { router.GET("/", nil) }) if recv == nil { t.Fatal("registering nil handler did not panic") } } func TestRouterRegexUserValues(t *testing.T) { mux := New() mux.GET("/metrics", func(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusOK) }) v4 := mux.Group("/v4") id := v4.Group("/{id:^[1-9]\\d*}") id.GET("/click", func(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusOK) }) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI("/v4/123/click") mux.Handler(ctx) v1 := ctx.UserValue("id") if v1 != "123" { t.Fatalf(`expected "123" in user value, got %q`, v1) } ctx.Request.Reset() ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI("/metrics") mux.Handler(ctx) if v1 != "123" { t.Fatalf(`expected "123" in user value after second call, got %q`, v1) } } func TestRouterChaining(t *testing.T) { router1 := New() router2 := New() router1.NotFound = router2.Handler fooHit := false router1.POST("/foo", func(ctx *fasthttp.RequestCtx) { fooHit = true ctx.SetStatusCode(fasthttp.StatusOK) }) barHit := false router2.POST("/bar", func(ctx *fasthttp.RequestCtx) { barHit = true ctx.SetStatusCode(fasthttp.StatusOK) }) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod(fasthttp.MethodPost) ctx.Request.SetRequestURI("/foo") router1.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusOK && fooHit) { t.Errorf("Regular routing failed with router chaining.") t.FailNow() } ctx.Request.Header.SetMethod(fasthttp.MethodPost) ctx.Request.SetRequestURI("/bar") router1.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusOK && barHit) { t.Errorf("Chained routing failed with router chaining.") t.FailNow() } ctx.Request.Header.SetMethod(fasthttp.MethodPost) ctx.Request.SetRequestURI("/qax") router1.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusNotFound) { t.Errorf("NotFound behavior failed with router chaining.") t.FailNow() } } func TestRouterMutable(t *testing.T) { handler1 := func(_ *fasthttp.RequestCtx) {} handler2 := func(_ *fasthttp.RequestCtx) {} router := New() router.Mutable(true) if !router.treeMutable { t.Errorf("Router.treesMutables is false") } for _, method := range httpMethods { router.Handle(method, "/", handler1) } for method := range router.trees { if !router.trees[method].Mutable { t.Errorf("Method %d - Mutable == %v, want %v", method, router.trees[method].Mutable, true) } } routes := []string{ "/", "/api/{version}", "/{filepath:*}", "/user{user:.*}", } router = New() for _, route := range routes { for _, method := range httpMethods { router.Handle(method, route, handler1) } for _, method := range httpMethods { err := catchPanic(func() { router.Handle(method, route, handler2) }) if err == nil { t.Errorf("Mutable 'false' - Method %s - Route %s - Expected panic", method, route) } h, _ := router.Lookup(method, route, nil) if reflect.ValueOf(h).Pointer() != reflect.ValueOf(handler1).Pointer() { t.Errorf("Mutable 'false' - Method %s - Route %s - Handler updated", method, route) } } router.Mutable(true) for _, method := range httpMethods { err := catchPanic(func() { router.Handle(method, route, handler2) }) if err != nil { t.Errorf("Mutable 'true' - Method %s - Route %s - Unexpected panic: %v", method, route, err) } h, _ := router.Lookup(method, route, nil) if reflect.ValueOf(h).Pointer() != reflect.ValueOf(handler2).Pointer() { t.Errorf("Method %s - Route %s - Handler is not updated", method, route) } } router.Mutable(false) } } func TestRouterOPTIONS(t *testing.T) { handlerFunc := func(_ *fasthttp.RequestCtx) {} router := New() router.POST("/path", handlerFunc) ctx := new(fasthttp.RequestCtx) var checkHandling = func(path, expectedAllowed string, expectedStatusCode int) { ctx.Request.Header.SetMethod(fasthttp.MethodOptions) ctx.Request.SetRequestURI(path) router.Handler(ctx) if !(ctx.Response.StatusCode() == expectedStatusCode) { t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", ctx.Response.StatusCode(), ctx.Response.Header.String()) } else if allow := string(ctx.Response.Header.Peek("Allow")); allow != expectedAllowed { t.Error("unexpected Allow header value: " + allow) } } // test not allowed // * (server) checkHandling("*", "OPTIONS, POST", fasthttp.StatusOK) // path checkHandling("/path", "OPTIONS, POST", fasthttp.StatusOK) ctx.Request.Header.SetMethod(fasthttp.MethodOptions) ctx.Request.SetRequestURI("/doesnotexist") router.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusNotFound) { t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", ctx.Response.StatusCode(), ctx.Response.Header.String()) } // add another method router.GET("/path", handlerFunc) // set a global OPTIONS handler router.GlobalOPTIONS = func(ctx *fasthttp.RequestCtx) { // Adjust status code to 204 ctx.SetStatusCode(fasthttp.StatusNoContent) } // test again // * (server) checkHandling("*", "GET, OPTIONS, POST", fasthttp.StatusNoContent) // path checkHandling("/path", "GET, OPTIONS, POST", fasthttp.StatusNoContent) // custom handler var custom bool router.OPTIONS("/path", func(ctx *fasthttp.RequestCtx) { custom = true }) // test again // * (server) checkHandling("*", "GET, OPTIONS, POST", fasthttp.StatusNoContent) if custom { t.Error("custom handler called on *") } // path ctx.Request.Header.SetMethod(fasthttp.MethodOptions) ctx.Request.SetRequestURI("/path") router.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusNoContent) { t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", ctx.Response.StatusCode(), ctx.Response.Header.String()) } if !custom { t.Error("custom handler not called") } } func TestRouterNotAllowed(t *testing.T) { handlerFunc := func(_ *fasthttp.RequestCtx) {} router := New() router.POST("/path", handlerFunc) ctx := new(fasthttp.RequestCtx) var checkHandling = func(path, expectedAllowed string, expectedStatusCode int) { ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI(path) router.Handler(ctx) if !(ctx.Response.StatusCode() == expectedStatusCode) { t.Errorf("NotAllowed handling failed:: Code=%d, Header=%v", ctx.Response.StatusCode(), ctx.Response.Header.String()) } else if allow := string(ctx.Response.Header.Peek("Allow")); allow != expectedAllowed { t.Error("unexpected Allow header value: " + allow) } } // test not allowed checkHandling("/path", "OPTIONS, POST", fasthttp.StatusMethodNotAllowed) // add another method router.DELETE("/path", handlerFunc) router.OPTIONS("/path", handlerFunc) // must be ignored // test again checkHandling("/path", "DELETE, OPTIONS, POST", fasthttp.StatusMethodNotAllowed) // test custom handler responseText := "custom method" router.MethodNotAllowed = func(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusTeapot) ctx.Write([]byte(responseText)) } ctx.Response.Reset() router.Handler(ctx) if got := string(ctx.Response.Body()); !(got == responseText) { t.Errorf("unexpected response got %q want %q", got, responseText) } if ctx.Response.StatusCode() != fasthttp.StatusTeapot { t.Errorf("unexpected response code %d want %d", ctx.Response.StatusCode(), fasthttp.StatusTeapot) } if allow := string(ctx.Response.Header.Peek("Allow")); allow != "DELETE, OPTIONS, POST" { t.Error("unexpected Allow header value: " + allow) } } func testRouterNotFoundByMethod(t *testing.T, method string) { handlerFunc := func(_ *fasthttp.RequestCtx) {} host := "fast" router := New() router.Handle(method, "/path", handlerFunc) router.Handle(method, "/dir/", handlerFunc) router.Handle(method, "/", handlerFunc) router.Handle(method, "/{proc}/StaTus", handlerFunc) router.Handle(method, "/USERS/{name}/enTRies/", handlerFunc) router.Handle(method, "/static/{filepath:*}", handlerFunc) reqMethod := method if method == MethodWild { reqMethod = randomHTTPMethod() } // Moved Permanently, request with GET method expectedCode := fasthttp.StatusMovedPermanently switch { case reqMethod == fasthttp.MethodConnect: // CONNECT method does not allow redirects, so Not Found (404) expectedCode = fasthttp.StatusNotFound case reqMethod != fasthttp.MethodGet: // Permanent Redirect, request with same method expectedCode = fasthttp.StatusPermanentRedirect } type testRoute struct { route string code int location string } testRoutes := []testRoute{ {"", fasthttp.StatusOK, ""}, // TSR +/ (Not clean by router, this path is cleaned by fasthttp `ctx.Path()`) {"/../path", expectedCode, buildLocation(host, "/path")}, // CleanPath (Not clean by router, this path is cleaned by fasthttp `ctx.Path()`) {"/nope", fasthttp.StatusNotFound, ""}, // NotFound } if method != fasthttp.MethodConnect { testRoutes = append(testRoutes, []testRoute{ {"/path/", expectedCode, buildLocation(host, "/path")}, // TSR -/ {"/dir", expectedCode, buildLocation(host, "/dir/")}, // TSR +/ {"/PATH", expectedCode, buildLocation(host, "/path")}, // Fixed Case {"/DIR/", expectedCode, buildLocation(host, "/dir/")}, // Fixed Case {"/PATH/", expectedCode, buildLocation(host, "/path")}, // Fixed Case -/ {"/DIR", expectedCode, buildLocation(host, "/dir/")}, // Fixed Case +/ {"/paTh/?name=foo", expectedCode, buildLocation(host, "/path?name=foo")}, // Fixed Case With Query Params +/ {"/paTh?name=foo", expectedCode, buildLocation(host, "/path?name=foo")}, // Fixed Case With Query Params +/ {"/sergio/status/", expectedCode, buildLocation(host, "/sergio/StaTus")}, // Fixed Case With Params -/ {"/users/atreugo/eNtriEs", expectedCode, buildLocation(host, "/USERS/atreugo/enTRies/")}, // Fixed Case With Params +/ {"/STatiC/test.go", expectedCode, buildLocation(host, "/static/test.go")}, // Fixed Case Wildcard }...) } for _, tr := range testRoutes { ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod(reqMethod) ctx.Request.SetRequestURI(tr.route) ctx.Request.SetHost(host) router.Handler(ctx) statusCode := ctx.Response.StatusCode() location := string(ctx.Response.Header.Peek("Location")) if !(statusCode == tr.code && (statusCode == fasthttp.StatusNotFound || location == tr.location)) { fn := t.Errorf msg := "NotFound handling route '%s' failed: Method=%s, ReqMethod=%s, Code=%d, ExpectedCode=%d, Header=%v" if runtime.GOOS == "windows" && strings.HasPrefix(tr.route, "/../") { // See: https://github.com/valyala/fasthttp/issues/1226 // Not fail, because it is a known issue. fn = t.Logf msg = "ERROR: " + msg } fn(msg, tr.route, method, reqMethod, statusCode, tr.code, location) } } ctx := new(fasthttp.RequestCtx) // Test custom not found handler var notFound bool router.NotFound = func(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusNotFound) notFound = true } ctx.Request.Header.SetMethod(reqMethod) ctx.Request.SetRequestURI("/nope") router.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusNotFound && notFound == true) { t.Errorf( "Custom NotFound handling failed: Method=%s, ReqMethod=%s, Code=%d, Header=%v", method, reqMethod, ctx.Response.StatusCode(), ctx.Response.Header.String(), ) } ctx.Response.Reset() } func TestRouterNotFound(t *testing.T) { for _, method := range httpMethods { testRouterNotFoundByMethod(t, method) } router := New() handlerFunc := func(_ *fasthttp.RequestCtx) {} host := "fast" ctx := new(fasthttp.RequestCtx) // Test other method than GET (want 308 instead of 301) router.PATCH("/path", handlerFunc) ctx.Request.Header.SetMethod(fasthttp.MethodPatch) ctx.Request.SetRequestURI("/path/?key=val") ctx.Request.SetHost(host) router.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusPermanentRedirect && string(ctx.Response.Header.Peek("Location")) == buildLocation(host, "/path?key=val")) { t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", ctx.Response.StatusCode(), ctx.Response.Header.String()) } ctx.Response.Reset() // Test special case where no node for the prefix "/" exists router = New() router.GET("/a", handlerFunc) ctx.Request.Header.SetMethod(fasthttp.MethodPatch) ctx.Request.SetRequestURI("/") router.Handler(ctx) if !(ctx.Response.StatusCode() == fasthttp.StatusNotFound) { t.Errorf("NotFound handling route / failed: Code=%d", ctx.Response.StatusCode()) } } func TestRouterNotFound_MethodWild(t *testing.T) { postFound, anyFound := false, false router := New() router.ANY("/{path:*}", func(ctx *fasthttp.RequestCtx) { anyFound = true }) router.POST("/specific", func(ctx *fasthttp.RequestCtx) { postFound = true }) for i := 0; i < 100; i++ { router.Handle( randomHTTPMethod(), fmt.Sprintf("/%d", rand.Int63()), func(ctx *fasthttp.RequestCtx) {}, ) } ctx := new(fasthttp.RequestCtx) var request = func(method, path string) { ctx.Request.Header.SetMethod(method) ctx.Request.SetRequestURI(path) router.Handler(ctx) } for _, method := range httpMethods { request(method, "/specific") if method == fasthttp.MethodPost { if !postFound { t.Errorf("Method '%s': not found", method) } } else { if !anyFound { t.Errorf("Method 'ANY' not found with request method %s", method) } } status := ctx.Response.StatusCode() if status != fasthttp.StatusOK { t.Errorf("Response status code == %d, want %d", status, fasthttp.StatusOK) } postFound, anyFound = false, false ctx.Response.Reset() } } func TestRouterPanicHandler(t *testing.T) { router := New() panicHandled := false router.PanicHandler = func(ctx *fasthttp.RequestCtx, p interface{}) { panicHandled = true } router.Handle(fasthttp.MethodPut, "/user/{name}", func(ctx *fasthttp.RequestCtx) { panic("oops!") }) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod(fasthttp.MethodPut) ctx.Request.SetRequestURI("/user/gopher") defer func() { if rcv := recover(); rcv != nil { t.Fatal("handling panic failed") } }() router.Handler(ctx) if !panicHandled { t.Fatal("simulating failed") } } func testRouterLookupByMethod(t *testing.T, method string) { reqMethod := method if method == MethodWild { reqMethod = randomHTTPMethod() } routed := false wantHandle := func(_ *fasthttp.RequestCtx) { routed = true } wantParams := map[string]string{"name": "gopher"} ctx := new(fasthttp.RequestCtx) router := New() // try empty router first handle, tsr := router.Lookup(reqMethod, "/nope", ctx) if handle != nil { t.Fatalf("Got handle for unregistered pattern: %v", handle) } if tsr { t.Error("Got wrong TSR recommendation!") } // insert route and try again router.Handle(method, "/user/{name}", wantHandle) handle, _ = router.Lookup(reqMethod, "/user/gopher", ctx) if handle == nil { t.Fatal("Got no handle!") } else { handle(nil) if !routed { t.Fatal("Routing failed!") } } for expectedKey, expectedVal := range wantParams { if ctx.UserValue(expectedKey) != expectedVal { t.Errorf("The values %s = %s is not save in context", expectedKey, expectedVal) } } routed = false // route without param router.Handle(method, "/user", wantHandle) handle, _ = router.Lookup(reqMethod, "/user", ctx) if handle == nil { t.Fatal("Got no handle!") } else { handle(nil) if !routed { t.Fatal("Routing failed!") } } for expectedKey, expectedVal := range wantParams { if ctx.UserValue(expectedKey) != expectedVal { t.Errorf("The values %s = %s is not save in context", expectedKey, expectedVal) } } handle, tsr = router.Lookup(reqMethod, "/user/gopher/", ctx) if handle != nil { t.Fatalf("Got handle for unregistered pattern: %v", handle) } if !tsr { t.Error("Got no TSR recommendation!") } handle, tsr = router.Lookup(reqMethod, "/nope", ctx) if handle != nil { t.Fatalf("Got handle for unregistered pattern: %v", handle) } if tsr { t.Error("Got wrong TSR recommendation!") } } func TestRouterLookup(t *testing.T) { for _, method := range httpMethods { testRouterLookupByMethod(t, method) } } func TestRouterMatchedRoutePath(t *testing.T) { route1 := "/user/{name}" routed1 := false handle1 := func(ctx *fasthttp.RequestCtx) { route := ctx.UserValue(MatchedRoutePathParam) if route != route1 { t.Fatalf("Wrong matched route: want %s, got %s", route1, route) } routed1 = true } route2 := "/user/{name}/details" routed2 := false handle2 := func(ctx *fasthttp.RequestCtx) { route := ctx.UserValue(MatchedRoutePathParam) if route != route2 { t.Fatalf("Wrong matched route: want %s, got %s", route2, route) } routed2 = true } route3 := "/" routed3 := false handle3 := func(ctx *fasthttp.RequestCtx) { route := ctx.UserValue(MatchedRoutePathParam) if route != route3 { t.Fatalf("Wrong matched route: want %s, got %s", route3, route) } routed3 = true } router := New() router.SaveMatchedRoutePath = true router.Handle(fasthttp.MethodGet, route1, handle1) router.Handle(fasthttp.MethodGet, route2, handle2) router.Handle(fasthttp.MethodGet, route3, handle3) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI("/user/gopher") router.Handler(ctx) if !routed1 || routed2 || routed3 { t.Fatal("Routing failed!") } ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI("/user/gopher/details") router.Handler(ctx) if !routed2 || routed3 { t.Fatal("Routing failed!") } ctx.Request.Header.SetMethod(fasthttp.MethodGet) ctx.Request.SetRequestURI("/") router.Handler(ctx) if !routed3 { t.Fatal("Routing failed!") } } func TestRouterServeFiles(t *testing.T) { r := New() recv := catchPanic(func() { r.ServeFiles("/noFilepath", os.TempDir()) }) if recv == nil { t.Fatal("registering path not ending with '{filepath:*}' did not panic") } body := []byte("fake ico") if err := os.WriteFile(os.TempDir()+"/favicon.ico", body, 0644); err != nil { t.Fatal(err) } r.ServeFiles("/{filepath:*}", os.TempDir()) assertWithTestServer(t, "GET /favicon.ico HTTP/1.1\r\n\r\n", r.Handler, func(rw *readWriter) { br := bufio.NewReader(&rw.w) var resp fasthttp.Response if err := resp.Read(br); err != nil { t.Fatalf("Unexpected error when reading response: %s", err) } if resp.Header.StatusCode() != 200 { t.Fatalf("Unexpected status code %d. Expected %d", resp.Header.StatusCode(), 200) } if !bytes.Equal(resp.Body(), body) { t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), string(body)) } }) } func TestRouterServeFS(t *testing.T) { r := New() recv := catchPanic(func() { r.ServeFS("/noFilepath", fsTestFilesystem) }) if recv == nil { t.Fatal("registering path not ending with '{filepath:*}' did not panic") } body, err := os.ReadFile("LICENSE") if err != nil { t.Fatal(err) } r.ServeFS("/{filepath:*}", fsTestFilesystem) assertWithTestServer(t, "GET /LICENSE HTTP/1.1\r\n\r\n", r.Handler, func(rw *readWriter) { br := bufio.NewReader(&rw.w) var resp fasthttp.Response if err := resp.Read(br); err != nil { t.Fatalf("Unexpected error when reading response: %s", err) } if resp.Header.StatusCode() != 200 { t.Fatalf("Unexpected status code %d. Expected %d", resp.Header.StatusCode(), 200) } if !bytes.Equal(resp.Body(), body) { t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), string(body)) } }) } func TestRouterServeFilesCustom(t *testing.T) { r := New() root := os.TempDir() fs := &fasthttp.FS{ Root: root, } recv := catchPanic(func() { r.ServeFilesCustom("/noFilepath", fs) }) if recv == nil { t.Fatal("registering path not ending with '{filepath:*}' did not panic") } body := []byte("fake ico") ioutil.WriteFile(root+"/favicon.ico", body, 0644) r.ServeFilesCustom("/{filepath:*}", fs) assertWithTestServer(t, "GET /favicon.ico HTTP/1.1\r\n\r\n", r.Handler, func(rw *readWriter) { br := bufio.NewReader(&rw.w) var resp fasthttp.Response if err := resp.Read(br); err != nil { t.Fatalf("Unexpected error when reading response: %s", err) } if resp.Header.StatusCode() != 200 { t.Fatalf("Unexpected status code %d. Expected %d", resp.Header.StatusCode(), 200) } if !bytes.Equal(resp.Body(), body) { t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), string(body)) } }) } func TestRouterList(t *testing.T) { expected := map[string][]string{ "GET": {"/bar"}, "PATCH": {"/foo"}, "POST": {"/v1/users/{name}/{surname?}"}, "DELETE": {"/v1/users/{id?}"}, } r := New() r.GET("/bar", func(ctx *fasthttp.RequestCtx) {}) r.PATCH("/foo", func(ctx *fasthttp.RequestCtx) {}) v1 := r.Group("/v1") v1.POST("/users/{name}/{surname?}", func(ctx *fasthttp.RequestCtx) {}) v1.DELETE("/users/{id?}", func(ctx *fasthttp.RequestCtx) {}) result := r.List() if !reflect.DeepEqual(result, expected) { t.Errorf("Router.List() == %v, want %v", result, expected) } } func TestRouterSamePrefixParamRoute(t *testing.T) { var id1, id2, id3, pageSize, page, iid string var routed1, routed2, routed3 bool r := New() v1 := r.Group("/v1") v1.GET("/foo/{id}/{pageSize}/{page}", func(ctx *fasthttp.RequestCtx) { id1 = ctx.UserValue("id").(string) pageSize = ctx.UserValue("pageSize").(string) page = ctx.UserValue("page").(string) routed1 = true }) v1.GET("/foo/{id}/{iid}", func(ctx *fasthttp.RequestCtx) { id2 = ctx.UserValue("id").(string) iid = ctx.UserValue("iid").(string) routed2 = true }) v1.GET("/foo/{id}", func(ctx *fasthttp.RequestCtx) { id3 = ctx.UserValue("id").(string) routed3 = true }) req := new(fasthttp.RequestCtx) req.Request.SetRequestURI("/v1/foo/1/20/4") r.Handler(req) req = new(fasthttp.RequestCtx) req.Request.SetRequestURI("/v1/foo/2/3") r.Handler(req) req = new(fasthttp.RequestCtx) req.Request.SetRequestURI("/v1/foo/v3") r.Handler(req) if !routed1 { t.Error("/foo/{id}/{pageSize}/{page} not routed.") } if !routed2 { t.Error("/foo/{id}/{iid} not routed") } if !routed3 { t.Error("/foo/{id} not routed") } if id1 != "1" { t.Errorf("/foo/{id}/{pageSize}/{page} id expect: 1 got %s", id1) } if pageSize != "20" { t.Errorf("/foo/{id}/{pageSize}/{page} pageSize expect: 20 got %s", pageSize) } if page != "4" { t.Errorf("/foo/{id}/{pageSize}/{page} page expect: 4 got %s", page) } if id2 != "2" { t.Errorf("/foo/{id}/{iid} id expect: 2 got %s", id2) } if iid != "3" { t.Errorf("/foo/{id}/{iid} iid expect: 3 got %s", iid) } if id3 != "v3" { t.Errorf("/foo/{id} id expect: v3 got %s", id3) } } func BenchmarkAllowed(b *testing.B) { handlerFunc := func(_ *fasthttp.RequestCtx) {} router := New() router.POST("/path", handlerFunc) router.GET("/path", handlerFunc) b.Run("Global", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = router.allowed("*", fasthttp.MethodOptions) } }) b.Run("Path", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = router.allowed("/path", fasthttp.MethodOptions) } }) } func BenchmarkRouterGet(b *testing.B) { r := New() r.GET("/hello", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/hello") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterParams(b *testing.B) { r := New() r.GET("/{id}", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/hello") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterANY(b *testing.B) { r := New() r.GET("/data", func(ctx *fasthttp.RequestCtx) {}) r.ANY("/", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterGet_ANY(b *testing.B) { resp := []byte("Bench GET") respANY := []byte("Bench GET (ANY)") r := New() r.GET("/", func(ctx *fasthttp.RequestCtx) { ctx.Success("text/plain", resp) }) r.ANY("/", func(ctx *fasthttp.RequestCtx) { ctx.Success("text/plain", respANY) }) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("UNICORN") ctx.Request.SetRequestURI("/") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterNotFound(b *testing.B) { r := New() r.GET("/bench", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/notfound") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterFindCaseInsensitive(b *testing.B) { r := New() r.GET("/bench", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/BenCh/.") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func BenchmarkRouterRedirectTrailingSlash(b *testing.B) { r := New() r.GET("/bench/", func(ctx *fasthttp.RequestCtx) {}) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/bench") for i := 0; i < b.N; i++ { r.Handler(ctx) } } func Benchmark_Get(b *testing.B) { handler := func(ctx *fasthttp.RequestCtx) {} r := New() r.GET("/", handler) r.GET("/plaintext", handler) r.GET("/json", handler) r.GET("/fortune", handler) r.GET("/fortune-quick", handler) r.GET("/db", handler) r.GET("/queries", handler) r.GET("/update", handler) ctx := new(fasthttp.RequestCtx) ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/update") b.ResetTimer() for i := 0; i < b.N; i++ { r.Handler(ctx) } } router-1.5.4/types.go000066400000000000000000000065121473534360100145460ustar00rootroot00000000000000package router import ( "github.com/fasthttp/router/radix" "github.com/valyala/fasthttp" ) // Router is a fasthttp.RequestHandler which can be used to dispatch requests to different // handler functions via configurable routes type Router struct { trees []*radix.Tree treeMutable bool customMethodsIndex map[string]int registeredPaths map[string][]string // If enabled, adds the matched route path onto the ctx.UserValue context // before invoking the handler. // The matched route path is only added to handlers of routes that were // registered when this option was enabled. SaveMatchedRoutePath bool // 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 308 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 308 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 // An optional fasthttp.RequestHandler that is called on automatic OPTIONS requests. // The handler is only called if HandleOPTIONS is true and no OPTIONS // handler for the specific path was set. // The "Allowed" header is set before calling the handler. GlobalOPTIONS fasthttp.RequestHandler // Configurable fasthttp.RequestHandler which is called when no matching route is // found. If it is not set, default NotFound is used. NotFound fasthttp.RequestHandler // Configurable fasthttp.RequestHandler which is called when a request // cannot be routed and HandleMethodNotAllowed is true. // If it is not set, ctx.Error with fasthttp.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{}) // Cached value of global (*) allowed methods globalAllowed string } // Group is a sub-router to group paths type Group struct { router *Router prefix string } router-1.5.4/utils.go000066400000000000000000000003041473534360100145330ustar00rootroot00000000000000package router import "strings" func validatePath(path string) { switch { case len(path) == 0 || !strings.HasPrefix(path, "/"): panic("path must begin with '/' in path '" + path + "'") } } router-1.5.4/utils_test.go000066400000000000000000000006641473534360100156030ustar00rootroot00000000000000package router import "testing" func Test_validatePath(t *testing.T) { if err := catchPanic(func() { validatePath("") }); err == nil { t.Error("an error was expected with an empty path") } if err := catchPanic(func() { validatePath("foo") }); err == nil { t.Error("an error was expected with an empty path") } if err := catchPanic(func() { validatePath("/foo") }); err != nil { t.Errorf("unexpected error: %v", err) } }