pax_global_header00006660000000000000000000000064144564733330014526gustar00rootroot0000000000000052 comment=d2b1b6d3a306b8905ddc2a870c65940700946ebe golang-github-labstack-echo-4.11.1/000077500000000000000000000000001445647333300170375ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/.editorconfig000066400000000000000000000007231445647333300215160ustar00rootroot00000000000000# EditorConfig coding styles definitions. For more information about the # properties used in this file, please see the EditorConfig documentation: # http://editorconfig.org/ # indicate this is the root of the project root = true [*] charset = utf-8 end_of_line = LF insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 [Makefile] indent_style = tab [*.md] trim_trailing_whitespace = false [*.go] indent_style = tab golang-github-labstack-echo-4.11.1/.gitattributes000066400000000000000000000012631445647333300217340ustar00rootroot00000000000000# Automatically normalize line endings for all text-based files # http://git-scm.com/docs/gitattributes#_end_of_line_conversion * text=auto # For the following file types, normalize line endings to LF on checking and # prevent conversion to CRLF when they are checked out (this is required in # order to prevent newline related issues) .* text eol=lf *.go text eol=lf *.yml text eol=lf *.html text eol=lf *.css text eol=lf *.js text eol=lf *.json text eol=lf LICENSE text eol=lf # Exclude `website` and `cookbook` from GitHub's language statistics # https://github.com/github/linguist#using-gitattributes cookbook/* linguist-documentation website/* linguist-documentation golang-github-labstack-echo-4.11.1/.github/000077500000000000000000000000001445647333300203775ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/.github/FUNDING.yml000066400000000000000000000012041445647333300222110ustar00rootroot00000000000000# These are supported funding model platforms github: [labstack] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] golang-github-labstack-echo-4.11.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000004301445647333300231010ustar00rootroot00000000000000### Issue Description ### Checklist - [ ] Dependencies installed - [ ] No typos - [ ] Searched existing issues and docs ### Expected behaviour ### Actual behaviour ### Steps to reproduce ### Working code to debug ```go package main func main() { } ``` ### Version/commit golang-github-labstack-echo-4.11.1/.github/stale.yml000066400000000000000000000013221445647333300222300ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 30 # Issues with these labels will never be considered stale exemptLabels: - pinned - security - bug - enhancement # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed within a month if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false golang-github-labstack-echo-4.11.1/.github/workflows/000077500000000000000000000000001445647333300224345ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/.github/workflows/checks.yml000066400000000000000000000017461445647333300244270ustar00rootroot00000000000000name: Run checks on: push: branches: - master pull_request: branches: - master workflow_dispatch: permissions: contents: read # to fetch code (actions/checkout) env: # run static analysis only with the latest Go version LATEST_GO_VERSION: "1.20" jobs: check: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v3 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v3 with: go-version: ${{ env.LATEST_GO_VERSION }} check-latest: true - name: Run golint run: | go install golang.org/x/lint/golint@latest golint -set_exit_status ./... - name: Run staticcheck run: | go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... - name: Run govulncheck run: | go version go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... golang-github-labstack-echo-4.11.1/.github/workflows/echo.yml000066400000000000000000000046241445647333300241030ustar00rootroot00000000000000name: Run Tests on: push: branches: - master pull_request: branches: - master workflow_dispatch: permissions: contents: read # to fetch code (actions/checkout) env: # run coverage and benchmarks only with the latest Go version LATEST_GO_VERSION: "1.20" jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy # Echo tests with last four major releases (unless there are pressing vulnerabilities) # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when # we derive from last four major releases promise. go: ["1.18", "1.19", "1.20"] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - name: Checkout Code uses: actions/checkout@v3 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: Run Tests run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v3 with: token: fail_ci_if_error: false benchmark: needs: test name: Benchmark comparison runs-on: ubuntu-latest steps: - name: Checkout Code (Previous) uses: actions/checkout@v3 with: ref: ${{ github.base_ref }} path: previous - name: Checkout Code (New) uses: actions/checkout@v3 with: path: new - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v3 with: go-version: ${{ env.LATEST_GO_VERSION }} - name: Install Dependencies run: go install golang.org/x/perf/cmd/benchstat@latest - name: Run Benchmark (Previous) run: | cd previous go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt - name: Run Benchmark (New) run: | cd new go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt - name: Run Benchstat run: | benchstat previous/benchmark.txt new/benchmark.txt golang-github-labstack-echo-4.11.1/.gitignore000066400000000000000000000000761445647333300210320ustar00rootroot00000000000000.DS_Store coverage.txt _test vendor .idea *.iml *.out .vscode golang-github-labstack-echo-4.11.1/CHANGELOG.md000066400000000000000000000517501445647333300206600ustar00rootroot00000000000000# Changelog ## v4.11.1 - 2023-07-16 **Fixes** * Fix `Gzip` middleware not sending response code for no content responses (404, 301/302 redirects etc) [#2481](https://github.com/labstack/echo/pull/2481) ## v4.11.0 - 2023-07-14 **Fixes** * Fixes the proxy middleware concurrency issue of calling the Next() proxy target on Round Robin Balancer [#2409](https://github.com/labstack/echo/pull/2409) * Fix `group.RouteNotFound` not working when group has attached middlewares [#2411](https://github.com/labstack/echo/pull/2411) * Fix global error handler return error message when message is an error [#2456](https://github.com/labstack/echo/pull/2456) * Do not use global timeNow variables [#2477](https://github.com/labstack/echo/pull/2477) **Enhancements** * Added a optional config variable to disable centralized error handler in recovery middleware [#2410](https://github.com/labstack/echo/pull/2410) * refactor: use `strings.ReplaceAll` directly [#2424](https://github.com/labstack/echo/pull/2424) * Add support for Go1.20 `http.rwUnwrapper` to Response struct [#2425](https://github.com/labstack/echo/pull/2425) * Check whether is nil before invoking centralized error handling [#2429](https://github.com/labstack/echo/pull/2429) * Proper colon support in `echo.Reverse` method [#2416](https://github.com/labstack/echo/pull/2416) * Fix misuses of a vs an in documentation comments [#2436](https://github.com/labstack/echo/pull/2436) * Add link to slog.Handler library for Echo logging into README.md [#2444](https://github.com/labstack/echo/pull/2444) * In proxy middleware Support retries of failed proxy requests [#2414](https://github.com/labstack/echo/pull/2414) * gofmt fixes to comments [#2452](https://github.com/labstack/echo/pull/2452) * gzip response only if it exceeds a minimal length [#2267](https://github.com/labstack/echo/pull/2267) * Upgrade packages [#2475](https://github.com/labstack/echo/pull/2475) ## v4.10.2 - 2023-02-22 **Security** * `filepath.Clean` behaviour has changed in Go 1.20 - adapt to it [#2406](https://github.com/labstack/echo/pull/2406) * Add `middleware.CORSConfig.UnsafeWildcardOriginWithAllowCredentials` to make UNSAFE usages of wildcard origin + allow cretentials less likely [#2405](https://github.com/labstack/echo/pull/2405) **Enhancements** * Add more HTTP error values [#2277](https://github.com/labstack/echo/pull/2277) ## v4.10.1 - 2023-02-19 **Security** * Upgrade deps due to the latest golang.org/x/net vulnerability [#2402](https://github.com/labstack/echo/pull/2402) **Enhancements** * Add new JWT repository to the README [#2377](https://github.com/labstack/echo/pull/2377) * Return an empty string for ctx.path if there is no registered path [#2385](https://github.com/labstack/echo/pull/2385) * Add context timeout middleware [#2380](https://github.com/labstack/echo/pull/2380) * Update link to jaegertracing [#2394](https://github.com/labstack/echo/pull/2394) ## v4.10.0 - 2022-12-27 **Security** * We are deprecating JWT middleware in this repository. Please use https://github.com/labstack/echo-jwt instead. JWT middleware is moved to separate repository to allow us to bump/upgrade version of JWT implementation (`github.com/golang-jwt/jwt`) we are using which we can not do in Echo core because this would break backwards compatibility guarantees we try to maintain. * This minor version bumps minimum Go version to 1.17 (from 1.16) due `golang.org/x/` packages we depend on. There are several vulnerabilities fixed in these libraries. Echo still tries to support last 4 Go versions but there are occasions we can not guarantee this promise. **Enhancements** * Bump x/text to 0.3.8 [#2305](https://github.com/labstack/echo/pull/2305) * Bump dependencies and add notes about Go releases we support [#2336](https://github.com/labstack/echo/pull/2336) * Add helper interface for ProxyBalancer interface [#2316](https://github.com/labstack/echo/pull/2316) * Expose `middleware.CreateExtractors` function so we can use it from echo-contrib repository [#2338](https://github.com/labstack/echo/pull/2338) * Refactor func(Context) error to HandlerFunc [#2315](https://github.com/labstack/echo/pull/2315) * Improve function comments [#2329](https://github.com/labstack/echo/pull/2329) * Add new method HTTPError.WithInternal [#2340](https://github.com/labstack/echo/pull/2340) * Replace io/ioutil package usages [#2342](https://github.com/labstack/echo/pull/2342) * Add staticcheck to CI flow [#2343](https://github.com/labstack/echo/pull/2343) * Replace relative path determination from proprietary to std [#2345](https://github.com/labstack/echo/pull/2345) * Remove square brackets from ipv6 addresses in XFF (X-Forwarded-For header) [#2182](https://github.com/labstack/echo/pull/2182) * Add testcases for some BodyLimit middleware configuration options [#2350](https://github.com/labstack/echo/pull/2350) * Additional configuration options for RequestLogger and Logger middleware [#2341](https://github.com/labstack/echo/pull/2341) * Add route to request log [#2162](https://github.com/labstack/echo/pull/2162) * GitHub Workflows security hardening [#2358](https://github.com/labstack/echo/pull/2358) * Add govulncheck to CI and bump dependencies [#2362](https://github.com/labstack/echo/pull/2362) * Fix rate limiter docs [#2366](https://github.com/labstack/echo/pull/2366) * Refactor how `e.Routes()` work and introduce `e.OnAddRouteHandler` callback [#2337](https://github.com/labstack/echo/pull/2337) ## v4.9.1 - 2022-10-12 **Fixes** * Fix logger panicing (when template is set to empty) by bumping dependency version [#2295](https://github.com/labstack/echo/issues/2295) **Enhancements** * Improve CORS documentation [#2272](https://github.com/labstack/echo/pull/2272) * Update readme about supported Go versions [#2291](https://github.com/labstack/echo/pull/2291) * Tests: improve error handling on closing body [#2254](https://github.com/labstack/echo/pull/2254) * Tests: refactor some of the assertions in tests [#2275](https://github.com/labstack/echo/pull/2275) * Tests: refactor assertions [#2301](https://github.com/labstack/echo/pull/2301) ## v4.9.0 - 2022-09-04 **Security** * Fix open redirect vulnerability in handlers serving static directories (e.Static, e.StaticFs, echo.StaticDirectoryHandler) [#2260](https://github.com/labstack/echo/pull/2260) **Enhancements** * Allow configuring ErrorHandler in CSRF middleware [#2257](https://github.com/labstack/echo/pull/2257) * Replace HTTP method constants in tests with stdlib constants [#2247](https://github.com/labstack/echo/pull/2247) ## v4.8.0 - 2022-08-10 **Most notable things** You can now add any arbitrary HTTP method type as a route [#2237](https://github.com/labstack/echo/pull/2237) ```go e.Add("COPY", "/*", func(c echo.Context) error return c.String(http.StatusOK, "OK COPY") }) ``` You can add custom 404 handler for specific paths [#2217](https://github.com/labstack/echo/pull/2217) ```go e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) }) g := e.Group("/images") g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) }) ``` **Enhancements** * Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to Valuebinder [#2127](https://github.com/labstack/echo/pull/2127) * Refactor: body_limit middleware unit test [#2145](https://github.com/labstack/echo/pull/2145) * Refactor: Timeout mw: rework how test waits for timeout. [#2187](https://github.com/labstack/echo/pull/2187) * BasicAuth middleware returns 500 InternalServerError on invalid base64 strings but should return 400 [#2191](https://github.com/labstack/echo/pull/2191) * Refactor: duplicated findStaticChild process at findChildWithLabel [#2176](https://github.com/labstack/echo/pull/2176) * Allow different param names in different methods with same path scheme [#2209](https://github.com/labstack/echo/pull/2209) * Add support for registering handlers for different 404 routes [#2217](https://github.com/labstack/echo/pull/2217) * Middlewares should use errors.As() instead of type assertion on HTTPError [#2227](https://github.com/labstack/echo/pull/2227) * Allow arbitrary HTTP method types to be added as routes [#2237](https://github.com/labstack/echo/pull/2237) ## v4.7.2 - 2022-03-16 **Fixes** * Fix nil pointer exception when calling Start again after address binding error [#2131](https://github.com/labstack/echo/pull/2131) * Fix CSRF middleware not being able to extract token from multipart/form-data form [#2136](https://github.com/labstack/echo/pull/2136) * Fix Timeout middleware write race [#2126](https://github.com/labstack/echo/pull/2126) **Enhancements** * Recover middleware should not log panic for aborted handler [#2134](https://github.com/labstack/echo/pull/2134) ## v4.7.1 - 2022-03-13 **Fixes** * Fix `e.Static`, `.File()`, `c.Attachment()` being picky with paths starting with `./`, `../` and `/` after 4.7.0 introduced echo.Filesystem support (Go1.16+) [#2123](https://github.com/labstack/echo/pull/2123) **Enhancements** * Remove some unused code [#2116](https://github.com/labstack/echo/pull/2116) ## v4.7.0 - 2022-03-01 **Enhancements** * Add JWT, KeyAuth, CSRF multivalue extractors [#2060](https://github.com/labstack/echo/pull/2060) * Add LogErrorFunc to recover middleware [#2072](https://github.com/labstack/echo/pull/2072) * Add support for HEAD method query params binding [#2027](https://github.com/labstack/echo/pull/2027) * Improve filesystem support with echo.FileFS, echo.StaticFS, group.FileFS, group.StaticFS [#2064](https://github.com/labstack/echo/pull/2064) **Fixes** * Fix X-Real-IP bug, improve tests [#2007](https://github.com/labstack/echo/pull/2007) * Minor syntax fixes [#1994](https://github.com/labstack/echo/pull/1994), [#2102](https://github.com/labstack/echo/pull/2102), [#2102](https://github.com/labstack/echo/pull/2102) **General** * Add cache-control and connection headers [#2103](https://github.com/labstack/echo/pull/2103) * Add Retry-After header constant [#2078](https://github.com/labstack/echo/pull/2078) * Upgrade `go` directive in `go.mod` to 1.17 [#2049](https://github.com/labstack/echo/pull/2049) * Add Pagoda [#2077](https://github.com/labstack/echo/pull/2077) and Souin [#2069](https://github.com/labstack/echo/pull/2069) to 3rd-party middlewares in README ## v4.6.3 - 2022-01-10 **Fixes** * Fixed Echo version number in greeting message which was not incremented to `4.6.2` [#2066](https://github.com/labstack/echo/issues/2066) ## v4.6.2 - 2022-01-08 **Fixes** * Fixed route containing escaped colon should be matchable but is not matched to request path [#2047](https://github.com/labstack/echo/pull/2047) * Fixed a problem that returned wrong content-encoding when the gzip compressed content was empty. [#1921](https://github.com/labstack/echo/pull/1921) * Update (test) dependencies [#2021](https://github.com/labstack/echo/pull/2021) **Enhancements** * Add support for configurable target header for the request_id middleware [#2040](https://github.com/labstack/echo/pull/2040) * Change decompress middleware to use stream decompression instead of buffering [#2018](https://github.com/labstack/echo/pull/2018) * Documentation updates ## v4.6.1 - 2021-09-26 **Enhancements** * Add start time to request logger middleware values [#1991](https://github.com/labstack/echo/pull/1991) ## v4.6.0 - 2021-09-20 Introduced a new [request logger](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) middleware to help with cases when you want to use some other logging library in your application. **Fixes** * fix timeout middleware warning: superfluous response.WriteHeader [#1905](https://github.com/labstack/echo/issues/1905) **Enhancements** * Add Cookie to KeyAuth middleware's KeyLookup [#1929](https://github.com/labstack/echo/pull/1929) * JWT middleware should ignore case of auth scheme in request header [#1951](https://github.com/labstack/echo/pull/1951) * Refactor default error handler to return first if response is already committed [#1956](https://github.com/labstack/echo/pull/1956) * Added request logger middleware which helps to use custom logger library for logging requests. [#1980](https://github.com/labstack/echo/pull/1980) * Allow escaping of colon in route path so Google Cloud API "custom methods" could be implemented [#1988](https://github.com/labstack/echo/pull/1988) ## v4.5.0 - 2021-08-01 **Important notes** A **BREAKING CHANGE** is introduced for JWT middleware users. The JWT library used for the JWT middleware had to be changed from [github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go) to [github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) due former library being unmaintained and affected by security issues. The [github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) project is a drop-in replacement, but supports only the latest 2 Go versions. So for JWT middleware users Go 1.15+ is required. For detailed information please read [#1940](https://github.com/labstack/echo/discussions/) To change the library imports in all .go files in your project replace all occurrences of `dgrijalva/jwt-go` with `golang-jwt/jwt`. For Linux CLI you can use: ```bash find -type f -name "*.go" -exec sed -i "s/dgrijalva\/jwt-go/golang-jwt\/jwt/g" {} \; go mod tidy ``` **Fixes** * Change JWT library to `github.com/golang-jwt/jwt` [#1946](https://github.com/labstack/echo/pull/1946) ## v4.4.0 - 2021-07-12 **Fixes** * Split HeaderXForwardedFor header only by comma [#1878](https://github.com/labstack/echo/pull/1878) * Fix Timeout middleware Context propagation [#1910](https://github.com/labstack/echo/pull/1910) **Enhancements** * Bind data using headers as source [#1866](https://github.com/labstack/echo/pull/1866) * Adds JWTConfig.ParseTokenFunc to JWT middleware to allow different libraries implementing JWT parsing. [#1887](https://github.com/labstack/echo/pull/1887) * Adding tests for Echo#Host [#1895](https://github.com/labstack/echo/pull/1895) * Adds RequestIDHandler function to RequestID middleware [#1898](https://github.com/labstack/echo/pull/1898) * Allow for custom JSON encoding implementations [#1880](https://github.com/labstack/echo/pull/1880) ## v4.3.0 - 2021-05-08 **Important notes** * Route matching has improvements for following cases: 1. Correctly match routes with parameter part as last part of route (with trailing backslash) 2. Considering handlers when resolving routes and search for matching http method handler * Echo minimal Go version is now 1.13. **Fixes** * When url ends with slash first param route is the match [#1804](https://github.com/labstack/echo/pull/1812) * Router should check if node is suitable as matching route by path+method and if not then continue search in tree [#1808](https://github.com/labstack/echo/issues/1808) * Fix timeout middleware not writing response correctly when handler panics [#1864](https://github.com/labstack/echo/pull/1864) * Fix binder not working with embedded pointer structs [#1861](https://github.com/labstack/echo/pull/1861) * Add Go 1.16 to CI and drop 1.12 specific code [#1850](https://github.com/labstack/echo/pull/1850) **Enhancements** * Make KeyFunc public in JWT middleware [#1756](https://github.com/labstack/echo/pull/1756) * Add support for optional filesystem to the static middleware [#1797](https://github.com/labstack/echo/pull/1797) * Add a custom error handler to key-auth middleware [#1847](https://github.com/labstack/echo/pull/1847) * Allow JWT token to be looked up from multiple sources [#1845](https://github.com/labstack/echo/pull/1845) ## v4.2.2 - 2021-04-07 **Fixes** * Allow proxy middleware to use query part in rewrite (#1802) * Fix timeout middleware not sending status code when handler returns an error (#1805) * Fix Bind() when target is array/slice and path/query params complains bind target not being struct (#1835) * Fix panic in redirect middleware on short host name (#1813) * Fix timeout middleware docs (#1836) ## v4.2.1 - 2021-03-08 **Important notes** Due to a datarace the config parameters for the newly added timeout middleware required a change. See the [docs](https://echo.labstack.com/middleware/timeout). A performance regression has been fixed, even bringing better performance than before for some routing scenarios. **Fixes** * Fix performance regression caused by path escaping (#1777, #1798, #1799, aldas) * Avoid context canceled errors (#1789, clwluvw) * Improve router to use on stack backtracking (#1791, aldas, stffabi) * Fix panic in timeout middleware not being not recovered and cause application crash (#1794, aldas) * Fix Echo.Serve() not serving on HTTP port correctly when TLSListener is used (#1785, #1793, aldas) * Apply go fmt (#1788, Le0tk0k) * Uses strings.Equalfold (#1790, rkilingr) * Improve code quality (#1792, withshubh) This release was made possible by our **contributors**: aldas, clwluvw, lammel, Le0tk0k, maciej-jezierski, rkilingr, stffabi, withshubh ## v4.2.0 - 2021-02-11 **Important notes** The behaviour for binding data has been reworked for compatibility with echo before v4.1.11 by enforcing `explicit tagging` for processing parameters. This **may break** your code if you expect combined handling of query/path/form params. Please see the updated documentation for [request](https://echo.labstack.com/guide/request) and [binding](https://echo.labstack.com/guide/request) The handling for rewrite rules has been slightly adjusted to expand `*` to a non-greedy `(.*?)` capture group. This is only relevant if multiple asterisks are used in your rules. Please see [rewrite](https://echo.labstack.com/middleware/rewrite) and [proxy](https://echo.labstack.com/middleware/proxy) for details. **Security** * Fix directory traversal vulnerability for Windows (#1718, little-cui) * Fix open redirect vulnerability with trailing slash (#1771,#1775 aldas,GeoffreyFrogeye) **Enhancements** * Add Echo#ListenerNetwork as configuration (#1667, pafuent) * Add ability to change the status code using response beforeFuncs (#1706, RashadAnsari) * Echo server startup to allow data race free access to listener address * Binder: Restore pre v4.1.11 behaviour for c.Bind() to use query params only for GET or DELETE methods (#1727, aldas) * Binder: Add separate methods to bind only query params, path params or request body (#1681, aldas) * Binder: New fluent binder for query/path/form parameter binding (#1717, #1736, aldas) * Router: Performance improvements for missed routes (#1689, pafuent) * Router: Improve performance for Real-IP detection using IndexByte instead of Split (#1640, imxyb) * Middleware: Support real regex rules for rewrite and proxy middleware (#1767) * Middleware: New rate limiting middleware (#1724, iambenkay) * Middleware: New timeout middleware implementation for go1.13+ (#1743, ) * Middleware: Allow regex pattern for CORS middleware (#1623, KlotzAndrew) * Middleware: Add IgnoreBase parameter to static middleware (#1701, lnenad, iambenkay) * Middleware: Add an optional custom function to CORS middleware to validate origin (#1651, curvegrid) * Middleware: Support form fields in JWT middleware (#1704, rkfg) * Middleware: Use sync.Pool for (de)compress middleware to improve performance (#1699, #1672, pafuent) * Middleware: Add decompress middleware to support gzip compressed requests (#1687, arun0009) * Middleware: Add ErrJWTInvalid for JWT middleware (#1627, juanbelieni) * Middleware: Add SameSite mode for CSRF cookies to support iframes (#1524, pr0head) **Fixes** * Fix handling of special trailing slash case for partial prefix (#1741, stffabi) * Fix handling of static routes with trailing slash (#1747) * Fix Static files route not working (#1671, pwli0755, lammel) * Fix use of caret(^) in regex for rewrite middleware (#1588, chotow) * Fix Echo#Reverse for Any type routes (#1695, pafuent) * Fix Router#Find panic with infinite loop (#1661, pafuent) * Fix Router#Find panic fails on Param paths (#1659, pafuent) * Fix DefaultHTTPErrorHandler with Debug=true (#1477, lammel) * Fix incorrect CORS headers (#1669, ulasakdeniz) * Fix proxy middleware rewritePath to use url with updated tests (#1630, arun0009) * Fix rewritePath for proxy middleware to use escaped path in (#1628, arun0009) * Remove unless defer (#1656, imxyb) **General** * New maintainers for Echo: Roland Lammel (@lammel) and Pablo Andres Fuente (@pafuent) * Add GitHub action to compare benchmarks (#1702, pafuent) * Binding query/path params and form fields to struct only works for explicit tags (#1729,#1734, aldas) * Add support for Go 1.15 in CI (#1683, asahasrabuddhe) * Add test for request id to remain unchanged if provided (#1719, iambenkay) * Refactor echo instance listener access and startup to speed up testing (#1735, aldas) * Refactor and improve various tests for binding and routing * Run test workflow only for relevant changes (#1637, #1636, pofl) * Update .travis.yml (#1662, santosh653) * Update README.md with an recents framework benchmark (#1679, pafuent) This release was made possible by **over 100 commits** from more than **20 contributors**: asahasrabuddhe, aldas, AndrewKlotz, arun0009, chotow, curvegrid, iambenkay, imxyb, juanbelieni, lammel, little-cui, lnenad, pafuent, pofl, pr0head, pwli, RashadAnsari, rkfg, santosh653, segfiner, stffabi, ulasakdeniz golang-github-labstack-echo-4.11.1/LICENSE000066400000000000000000000020631445647333300200450ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021 LabStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-labstack-echo-4.11.1/Makefile000066400000000000000000000021101445647333300204710ustar00rootroot00000000000000PKG := "github.com/labstack/echo" PKG_LIST := $(shell go list ${PKG}/...) tag: @git tag `grep -P '^\tversion = ' echo.go|cut -f2 -d'"'` @git tag|grep -v ^v .DEFAULT_GOAL := check check: lint vet race ## Check project init: @go install golang.org/x/lint/golint@latest @go install honnef.co/go/tools/cmd/staticcheck@latest lint: ## Lint the files @staticcheck ${PKG_LIST} @golint -set_exit_status ${PKG_LIST} vet: ## Vet the files @go vet ${PKG_LIST} test: ## Run tests @go test -short ${PKG_LIST} race: ## Run tests with data race detector @go test -race ${PKG_LIST} benchmark: ## Run benchmarks @go test -run="-" -bench=".*" ${PKG_LIST} help: ## Display this help screen @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' goversion ?= "1.17" test_version: ## Run tests inside Docker with given version (defaults to 1.17 oldest supported). Example: make test_version goversion=1.17 @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check" golang-github-labstack-echo-4.11.1/README.md000066400000000000000000000215101445647333300203150ustar00rootroot00000000000000 [![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v4) [![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo) [![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://github.com/labstack/echo/discussions) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) ## Supported Go versions Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with older versions. As of version 4.0.0, Echo is available as a [Go module](https://github.com/golang/go/wiki/Modules). Therefore a Go version capable of understanding /vN suffixed imports is required: Any of these versions will allow you to import Echo as `github.com/labstack/echo/v4` which is the recommended way of using Echo going forward. For older versions, please use the latest v3 tag. ## Feature Overview - Optimized HTTP router which smartly prioritize routes - Build robust and scalable RESTful APIs - Group APIs - Extensible middleware framework - Define middleware at root, group or route level - Data binding for JSON, XML and form payload - Handy functions to send variety of HTTP responses - Centralized HTTP error handling - Template rendering with any template engine - Define your format for the logger - Highly customizable - Automatic TLS via Let’s Encrypt - HTTP/2 support ## Benchmarks Date: 2020/11/11
Source: https://github.com/vishr/web-framework-benchmark
Lower is better! The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz ## [Guide](https://echo.labstack.com/guide) ### Installation ```sh // go get github.com/labstack/echo/{version} go get github.com/labstack/echo/v4 ``` ### Example ```go package main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "net/http" ) func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", hello) // Start server e.Logger.Fatal(e.Start(":1323")) } // Handler func hello(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") } ``` # Official middleware repositories Following list of middleware is maintained by Echo team. | Repository | Description | |------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt) | [JWT](https://github.com/golang-jwt/jwt) middleware | | [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) | [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](https://github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | # Third-party middleware repositories Be careful when adding 3rd party middleware. Echo teams does not have time or manpower to guarantee safety and quality of middlewares in this list. | Repository | Description | |------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator | | [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0. | | [github.com/ziflex/lecho](https://github.com/ziflex/lecho) | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface. | | [github.com/brpaz/echozap](https://github.com/brpaz/echozap) | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface. | | [github.com/samber/slog-echo](https://github.com/samber/slog-echo) | Go [slog](https://pkg.go.dev/golang.org/x/exp/slog) logging library wrapper for Echo logger interface. | | [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs. | | [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda) | Rapid, easy full-stack web development starter kit built with Echo. | | [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo) | ProtoBuf generate Echo server side code | Please send a PR to add your own library here. ## Help - [Forum](https://github.com/labstack/echo/discussions) ## Contribute **Use issues for everything** - For a small change, just send a PR. - For bigger changes open an issue for discussion before sending a PR. - PR should have: - Test case - Documentation - Example (If it makes sense) - You can also contribute by: - Reporting issues - Suggesting new features or enhancements - Improve/fix documentation ## Credits - [Vishal Rana](https://github.com/vishr) (Author) - [Nitin Rana](https://github.com/nr17) (Consultant) - [Roland Lammel](https://github.com/lammel) (Maintainer) - [Martti T.](https://github.com/aldas) (Maintainer) - [Pablo Andres Fuente](https://github.com/pafuent) (Maintainer) - [Contributors](https://github.com/labstack/echo/graphs/contributors) ## License [MIT](https://github.com/labstack/echo/blob/master/LICENSE) golang-github-labstack-echo-4.11.1/_fixture/000077500000000000000000000000001445647333300206645ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/_fixture/_fixture/000077500000000000000000000000001445647333300225115ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/_fixture/_fixture/README.md000066400000000000000000000000651445647333300237710ustar00rootroot00000000000000This directory is used for the static middleware testgolang-github-labstack-echo-4.11.1/_fixture/certs/000077500000000000000000000000001445647333300220045ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/_fixture/certs/README.md000066400000000000000000000006021445647333300232610ustar00rootroot00000000000000To generate a valid certificate and private key use the following command: ```bash # In OpenSSL ≥ 1.1.1 openssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \ -keyout key.pem -out cert.pem -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" ``` To check a certificate use the following command: ```bash openssl x509 -in cert.pem -text ``` golang-github-labstack-echo-4.11.1/_fixture/certs/cert.pem000066400000000000000000000035161445647333300234510ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIUaTvDluaMf+VJgYHQ0HFTS3yuCHYwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDIyNzIxMzQ0MVoXDTQ4MDcx NDIxMzQ0MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF AAOCAg8AMIICCgKCAgEAnqyyAAnWFH2TH7Epj5yfZxYrBvizydZe1Wo/1WpGR2IK QT+qIul5sEKX/ERqEOXsawSrL3fw9cuSM8Z2vD/57ZZdoSR7XIdVaMDEQenJ968a HObu4D27uBQwIwrM5ELgnd+fC4gis64nIu+2GSfHumZXi7lLW7DbNm8oWkMqI6tY 2s2wx2hwGYNVJrwSn4WGnkzhQ5U5mkcsLELMx7GR0Qnv6P7sNGZVeqMU7awkcSpR crKR1OUP7XCJkEq83WLHSx50+QZv7LiyDmGnujHevRbdSHlcFfHZtaufYat+qICe S3XADwRQe/0VSsmja6u3DAHy7VmL8PNisAdkopQZrhiI9OvGrpGZffs9zn+s/jeX N1bqVDihCMiEjqXMlHx2oj3AXrZTFxb7y7Ap9C07nf70lpxQWW9SjMYRF98JBiHF eJbQkNVkmz6T8ielQbX0l46F2SGK98oyFCGNIAZBUdj5CcS1E6w/lk4t58/em0k7 3wFC5qg0g0wfIbNSmxljBNxnaBYUqyaaAJJhpaEoOebm4RYV58hQ0FbMfpnLnSh4 dYStsk6i1PumWoa7D45DTtxF3kH7TB3YOB5aWaNGAPQC1m4Qcd23YB5Rd/ABirSp ux6/cFGosjSfJ/G+G0RhNUpmcbDJvFSOhD2WCuieVhCTAzp+VPIA9bSqD+InlT0C AwEAAaOBgTB/MB0GA1UdDgQWBBQZyM//SvzYKokQZI/0MVGb6PkH+zAfBgNVHSME GDAWgBQZyM//SvzYKokQZI/0MVGb6PkH+zAPBgNVHRMBAf8EBTADAQH/MCwGA1Ud EQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG 9w0BAQsFAAOCAgEAKGAJQmQ/KLw8iMb5QsyxxAonVjJ1eDAhNM3GWdHpM0/GFamO vVtATLQQldwDiZJvrsCQPEc8ctZ2Utvg/StLQ3+rZpsvt0+gcUlLJK61qguwYqb2 +T7VK5s7V/OyI/tsuboOW50Pka9vQHV+Z0aM06Yu+HNDAq/UTpEOb/3MQvZd6Ooy PTpZtFb/+5jIQa1dIsfFWmpBxF0+wUd9GEkX3j7nekwoZfJ8Ze4GWYERZbOFpDAQ rIHdthH5VJztnpQJmaKqzgIOF+Rurwlp5ecSC33xNNjDaYtuf/fiWnoKGhHVSBhT 61+0yxn3rTgh/Dsm95xY00rSX6lmcvI+kRNTUc8GGPz0ajBH6xyY7bNhfMjmnSW/ C/XTEDbTAhT7ndWC5vvzp7ZU0TvN+WY6A0f2kxSnnrEk6QRUvRtKkjAkmAFz8exi ttBBW0I3E5HNIC5CYRimq/9z+3clM/P1KbNblwuC65bL+PZ+nzFnn5hFaK9eLPol OwZQXv7IvAw8GfgLTrEUT7eBCQwe1IqesA7NTxF1BVwmNUb2XamvQZ7ly67QybRw 0uJq80XjpVjBWYTTQy1dsnC2OTKdqGsV9TVIDR+UGfIG9cxL70pEbiSH2AX+IDCy i3kNIvpXgBliAyOjW6Hj1fv6dNfAat/hqEfnquWkfvcs3HNrG/InwpwNAUs= -----END CERTIFICATE----- golang-github-labstack-echo-4.11.1/_fixture/certs/key.pem000066400000000000000000000063101445647333300232770ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCerLIACdYUfZMf sSmPnJ9nFisG+LPJ1l7Vaj/VakZHYgpBP6oi6XmwQpf8RGoQ5exrBKsvd/D1y5Iz xna8P/ntll2hJHtch1VowMRB6cn3rxoc5u7gPbu4FDAjCszkQuCd358LiCKzrici 77YZJ8e6ZleLuUtbsNs2byhaQyojq1jazbDHaHAZg1UmvBKfhYaeTOFDlTmaRyws QszHsZHRCe/o/uw0ZlV6oxTtrCRxKlFyspHU5Q/tcImQSrzdYsdLHnT5Bm/suLIO Yae6Md69Ft1IeVwV8dm1q59hq36ogJ5LdcAPBFB7/RVKyaNrq7cMAfLtWYvw82Kw B2SilBmuGIj068aukZl9+z3Of6z+N5c3VupUOKEIyISOpcyUfHaiPcBetlMXFvvL sCn0LTud/vSWnFBZb1KMxhEX3wkGIcV4ltCQ1WSbPpPyJ6VBtfSXjoXZIYr3yjIU IY0gBkFR2PkJxLUTrD+WTi3nz96bSTvfAULmqDSDTB8hs1KbGWME3GdoFhSrJpoA kmGloSg55ubhFhXnyFDQVsx+mcudKHh1hK2yTqLU+6ZahrsPjkNO3EXeQftMHdg4 HlpZo0YA9ALWbhBx3bdgHlF38AGKtKm7Hr9wUaiyNJ8n8b4bRGE1SmZxsMm8VI6E PZYK6J5WEJMDOn5U8gD1tKoP4ieVPQIDAQABAoICAEHF2CsH6MOpofi7GT08cR7s I33KTcxWngzc9ATk/qjMTO/rEf1Sxmx3zkR1n3nNtQhPcR5GG43nin0HwWQbKOCB OeJ4GuKp/o9jiHbCEEQpQyvD1jUBofSV+bYs3e2ogy8t6OGA1tGgWPy0XMlkoff0 QEnczw3864FO5m0z9h2/Ax//r02ZTw5kUEG0KAwT709jEuVO0AfRhM/8CKKmSola EyaDtSmrWbdyLlSuzJRUNFrVBno3UTjdM0iqkks6jN3ojBhFwNNhY/1uIXafAXNk LOnD1JYMIHCb6X809VWnqvYgozIWWb5rlA3iM2mITmId1LLqMYX5fWj2R5LUzSek H+XG+F9FIouTaL1ACoXr0zyeY5N5YJdyXYa1tThdW+axX9ZrnPgeiQrmxzKPIyb7 LLlVtNBQUg/t5tX80KyYjkNUu4j3oq/uBYPi0m//ovwMyi9bSbbyPT+cDXuXX5Bc oY7wyn3evXX0c1R7vdJLZLkLu+ctVex/9hvMjeW/mMasDjLnqY7pF3Skct1SX5N2 U8YVU9bGvFpLEwM9lmi/T7bcv+zbmGPlfTsZiFrCsixPLn7sX7y5M4L8au8O0jh0 nHm/8rWVg1Qw0Hobg3tA8FjeMa8Sr2fYmkNLVKFzhuJLxknTJLaUbX5CymNqWP4H OctvfSY0nSZ1eQpBkQaJAoIBAQDTb/NhYCfaJBLXHVMy/VYd7kWGZ+I87artcE/l 8u0pJ8XOP4kp0otFIumpHUFodysAeP6HrI79MuJB40fy91HzWZC+NrPufFFFuZ0z Ld1o3Y5nAeoZmMlf1F12Oe3OQZy7nm9eNNkfeoVtKqDv4FhAqk+aoMor86HscKsR C6HlZFdGc7kX0ylrQAXPq9KLhcvUU9oAUpbqTbhYK83IebRJgFDG45HkVo9SUHpF dmCFSb91eZpRGpdfNLCuLiSu52TebayaUCnceeAt8SyeiChJ/TwWmRRDJS0QUv6h s3Wdp+cx9ANoujA4XzAs8Fld5IZ4bcG5jjwD62/tJyWrCC5DAoIBAQDAHfHjrYCK GHBrMj+MA7cK7fCJUn/iJLSLGgo2ANYF5oq9gaCwHCtKIyB9DN/KiY0JpJ6PWg+Q 9Difq23YXiJjNEBS5EFTu9UwWAr1RhSAegrfHxm0sDbcAx31NtDYvBsADCWQYmzc KPfBshf5K4g/VCIj2VzC2CE6kNtdhqLU6AV2Pi1Tl1S82xWoAjHy91tDmlFQNWCj B2ZnZ7tY9zuwDfeBBOVCPHICgl5Q4PrY1KEWEXiNxgbtkNmOPAsY9WSqgOsP9pWK J924gdCCvovINzZtgRisxKth6Fkhra+VCsheg9SWvgR09Deo6CCoSwYxOSb0cjh2 oyX5Rb1kJ7Z/AoIBAQCX2iNVoBV/GcFeNXV3fXLH9ESCj0FwuNC1zp/TanDhyerK gd8k5k2Xzcc66gP73vpHUJ6dGlVni4/r+ivGV9HHkF/f/LGlaiuEhBZel2YY1mZb nIhg8dZOuNqW+mvMYlsKdHNPmW0GqpwBF0iWfu1jI+4gA7Kvdj6o7RIvH8eaVEJK GvqoHcP1fvmteJ2yDtmhGMfMy4QPqtnmmS8l+CJ/V2SsMuyorXIpkBsAoFAZ6ilT WY53CT4F5nWt4v39j7pl9SatfT1TV0SmOjvtb6Rf3zu0jyR6RMzkmHa/839ZRylI OxPntzDCi7qxy7yjLmlVPJ6RgZGgzwqHrEHlX+65AoIBAQCEzu6d3x5B2N02LZli eFr8MjqbI64GLiulEY5HgNJzZ8k3cjocJI0Ehj36VIEMaYRXSzbVkIO8SCgwsPiR n5mUDNX+t441jV62Odbxcc3Qdw226rABieOSupDmKEu92GOt57e8FV5939BOVYhf FunsJYQoViXbCEAIVYVgJSfBmNfVwuvgonfQyn8xErtm4/pyRGa71PqGGSKAj2Qi /16CuVUFGtZFsLV76JW8wZqHdI4bTF6TW3cEmaLbwcRGL7W0bMSS13rO8/pBh3QW PhUxhoGYt6rQHHEBkPa04nXDyZ10QRwgTSGVnBIyMK4KyTpxorm8OI2x7dzdcomX iCCPAoIBAETwfr2JKPb/AzrKhhbZgU+sLVn3WH/nb68VheNEmGOzsqXaSHCR2NOq /ow7bawjc8yUIhBRzokR4F/7jGolOmfdq0MYFb6/YokssKfv1ugxBhmvOxpZ6F6E cERJ8Ex/ffQU053gLR/0ammddVuS1GR5I/jEdP0lJVh0xapoZNUlT5dWYCgo20hY ZAmKpU+veyUn+5Li0pmm959vnLK5LJzEA5mpz3w1QPPtVwQs05dwmEV3CRAcCeeh 8sXp49WNCSW4I3BxuTZzRV845SGIFhZwgVV42PTp2LPKl2p6E7Bk8xpUCCvBpALp QmA5yIMx+u2Jpr7fUsXEXEPTEhvjff0= -----END PRIVATE KEY----- golang-github-labstack-echo-4.11.1/_fixture/favicon.ico000066400000000000000000000021761445647333300230130ustar00rootroot00000000000000 h(      & ))) + ( !'!' &!''  (#-"+!$.#- ' ?JWHOT 1AW9c=j"?h"?g#Cn#8m)5[FT^BPb-Io-Io7V{]Qn#:\q1erz/+8@7DMYckXdmEsw}hYbPUf?M_wp{釉T_eBZbhQ^hL^ipu~omsivszcX_vx_jr?`ipYqTqft~C_nyIr|ryfs}IK]iM^igolang-github-labstack-echo-4.11.1/_fixture/folder/000077500000000000000000000000001445647333300221375ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/_fixture/folder/index.html000066400000000000000000000001721445647333300241340ustar00rootroot00000000000000 Echo golang-github-labstack-echo-4.11.1/_fixture/images/000077500000000000000000000000001445647333300221315ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/_fixture/images/walle.png000066400000000000000000006553551445647333300237660ustar00rootroot00000000000000PNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FPIDATxwl}։~߰b];O}:խ,K%'Iƀ1\`0&y掹\ 00`2ۀql9Yd$Kέ>}>i+񮪳[i^>{VZGxiѢE-Z SТE-Z @-ZhѢMZhѢEmТE-Zh-ZhѢEhѢE-E-Zh&-ZhѢE6hѢE-ZnO=/IeMUh-R1{ʲF)!$q9ϼEIPֆ8DB $ZIűh;R8R2&|8B#h^~H!0a%4Ye,ZIx%%181'Қ8n>YՆټX>&y&q|Y.Gai彿YIDD()ὧ B\8_Rc"߄)ʪ"MbR =aLxX){ϳk0o_-EIx,:̕o>{u$&Kc"nbe^VR4|tVPT5ei16 J)N;CA{A󎪬@ KiJZu BxHI#AI:1Ion֒ yxVP׆5eYS[u>C B<(zBwx2pآE6hz/_=n:/޶8-yAk78sb;fǹ:!'ʼnq IE gΞ+W(yVW<ЃloFx)IAP +T<^DV$( ;+}6)<8ʪ(jEIQF9yEUe1a`C^#|Vk!=~b%h&-Z6 ?W 3Fc k~FmYx2=NlR|%fo>B66(멠u`h<'^6v̋~WyFGJ)R;$?%P.(9Z#VryNQeα[xDʣ#xg1QUd6/M&s*ckCU['*<ǽ ?|%wk6hѢDNcNE;ϠG><pJ'X]]%Ib8a2s{~~!,RN ^9k >$h-E6ʢ(@n ID nT1YST՝#zOoo 7cH)Ӝl3,e~dmmHǐB"d  s/@ ZGQoFRJƿ4A#vXSI*뙆~xphY+_Xqa<3OŇ\%&xS՟Bs ~xvӠEh \]-Zt4XkNP #ZRU5e)F,12+*3:>odXZ{X@`E8N;677֚c:9n4R *R cރA~/D)|-EG!9A% AqHm!+Kz}6 o eU3L&ɬdo8xsNR Ig<`emu|lFka`"Znu) KE@GKxAFLjg<%Җ6HhlV1/*&" juN?<$[mТ+?'YQ!IcM%r:YVCJS'm=pR`u@]VuPn[\6x[Lۡ""$ʗ :#y!~7!%ANHb@"R&i/ZbmVTOU8,$g6-~dYF .HUU3)~sx}H)\zB!)I8y+;H.0.8jH`i@;|2U3J鈨Yޅ@<;(r`E;v4˸}{~Uvn^& "Б;Nx0iǏq)y1c:RYFBJe C@IDKt ۛ'1dFQVF#R륔?mAF`ҡ^'El M%,&Ex4uye,$q/ ȝ'q}ӈ}Za ob9R ɐg:1_'csXoEp,2.:.( "8q&K$[A?"M4Ib6/K_/6lij&-Z<*2+?TMG^7%YLhs6&8wE<5t]++di`(!ofZҫX(YAH)7~b6-:'O1JSOk0O< vL ~*%IB)+{EtgN\x|d2e2˿~666xh8yG,5 ] p 0EI)Hp~gfx=onmТފ?[Ӣ[%J)4!M ^(%2_VjIRdh?^tteV]mmᛪ:Q3PԢ 9~1EiB?{Rʥ"ہ'Jbq"|YڥT({#KV!RޱHԩ=gz'O?ֱ-׏sM{Ç>KH[[ǹ*Jz(Psʪ`2q8? 4U^?ĩSk t{﹗z,xQJŚ@.pP(dCIqPQcNd織M6)ʚWZF?r)k&-ZBFk:/yOt/w<:MZtİqFAK?פIBmТ|/e@UwD"5~n "ƈNf(<: q46xDӗ3X^àOFI4ԁ*s-S']g{K=w?}=&RJDkq7 U%{ !4Rޑc͉΀ן>>/|51wwxO o0͗'OAͨ#km=8$Ϫ\Y|UӍcΝ$nrNmd?=蘆'n&-Z7ϗ R@3H)YojuREčH_8g=}!~©Oɦ݇!ڝI`+:kDʰ0ΩYt Yv^RRc ˘(E_H()߶a PGi8nzI +_R#s wu|c؏(.gss_?E 8Y0YsfzNi8gxAw8uYA>'8֊ݽ=?[67֙cHP Pu!V$hDefR&i7N] ƿ-EA-B?8kCuR4"Oc$&WISs!FGH5(H!F4aA.WٻX%rsԱNᤔ% `X8]:qʍ%_ +%ʟ AS+T)#$!Gi$8rss>kQtc6!w3Ν/B.()cY38o>,8X9! vnr|WA|9VeY"9NP$I$(AUhuRi HE;>~ ߤ{smТo??\S+AߡIB.iw([PuH!N6;M^!i. <ٴbk M;}q,n.>9)9zEMQF  `8)9Ng^#co:6F!"$'ArW?̻8"jZPtqYy??}w_q.\x?{~Hsx7pz>9.QCn/"{oq48op _\WV%Lk _¸ 0D (:KEoHI$%AIc uUSYMl[{s6mТx'J7Rז$Ҭ t$Dq*.J B6ok]q_ ܍_얇B]fL\ J( VAq;I~ʊLW:= -g)ZќGcNyp_ 0HeX4(Kֱ-EͿlmmr]wSQV[|ٗw|}#N?P8ٙrsG` `'=f6s'{Lyw S.ށwy|?*!7o0._/޹z5 6İ>.ghl:pȰL~7cVTܸO%*%Dl&-ZNsN#{im)k+],%ut2ZVT*\M \;^6f&@6 sX"D놉RF_`+'"kwzJrcwJϱCk8Ox?/FƖֵd#Q#(DF~FQKqnu}6Y__{_?OGX=Żk7p41o`ZY|t3E% ISc\MhflAwm6%hvvRnj0j鐲ր}9ґ@-qݑAp"EUqsKwC?MΜޤ 7_gqN<|>T qYtHZwlll?3-H*Ր5EBuooF6)˒gO,EXKmt ;E1'-,# mj,$;etd"8NҘ^'O޷-E`+у^X밾; ꑭlt/Zݪ‰$h2!XL쾮 R#¿9BEBhSH2B bZ2h,6@#tLӘĘQ{$&I]ͧ Is(6,pO:jtDHtor5~?f}mWW?2X&z"k%({ G#2 $n$r)E9 [ƭ3 c }.?ɼq-xUt;fGxCL#X<O>,%2:y:~:G4)*R;lno?n?-E6ۜs_kxz݌AC։:[HC\yYsx8\|hH1Suc{ձs`%M3,e0X̙Ӝ8vUh4  `C44(,s&$x& DLnph|pᲩ_R g*0%jR;+ZI*۷ɒN'鲹F0տal,+ʲpobakk syyd:( 'W4GEvB[~Rt֨g$&M"eMQ(%.xueu])A/֔@GiJ /(U$qtV=Y?Mhd\8w|4>_'ͫLg3Ŕt|>*\鈒 ` *;u.?&Y l}!=qKzÂџ_=ͽ'I/^ $D56pQR5 HF eUReɑ8E8޳{0}Μֹ-ݢMZx5wfM#I36:_"..jǜ>}_|?>I}.kV׈⌺TuXk" HD e<3)˒tƣ~G?In|6{L&ƀs1|y01wo)3QGYS՛LmuKd2Wk2iMiW?1I2K'`A\vkC"0/J`7HJI2-,)Bo1/mТŧU4WbmbW{tSd| *ԕަ+<#Yɓ۬{; .vw"U`ƩSk?v{/'th4b:+<}E lh#NzQ4~%xT=3Rx)*CY[];C9/H$j1 tR=sp*Bkud2$rs ѝ x' VX[_呏> >m_V !3sEėu8_0eOY1ڋWy bE`I# )7ogIE67)jg{<old%,qmVVz78}y}aь#GLLn)"mu3Ɠ9єA&ǽEhps޽+*r'MM]m^VZk.^/\|￧*Μ>ꀳgo̳7x{~[ހ VkIx[꺤(\zO>$xyo{\:szш'zWo} ϝe8ag,xfA Nz̋+M AZ,Vtce:5FXI5V8jb֘u$~VE"{sYz9s!?Odl ^7$<))W>훻L$ ;R45(f)t2M66ȳ,X[[͛4Ts D4c2sWyӛBkNukEcMPW%%*IBPeY2/ R"#:%=v~j턫XuK)\o?-E [l;AGtk-VduINYQnٳgٟ%>}gNq966O=g OW9{cY75ɄW1^H=L]ReU0/G?ݼ˾׿>+dxϼgyx_l6[GKv13:59jcfuMhpTa꒲B?i%%$I"I eYc?EEih<,]y$暴y5c++;Ƨ{lʵr`B` '£P8A[LV_cMҔU1䎢a̒d$N‹|Λ_CwM'I_)y͚1CxͲk$)IEL|f`LY4ô @6h &R' SV:a}̸:cX˝>6qΟgkkマO=w2o{(X!H|RV4VlmX]; i;}׫,N^wxF$Mh,yG9yN&TRU!%Em@QPf%ޅ@I( AOE(&IQ[יM'drDqȲ}a6H3hDb^߭c<Ԧ&ђچcOIEđ8KUܺq ]KiY/Mh+ڐ 5:1= t iÙ9Cw& \geU2R(:r^O$uD%() kp@kFQ-4΃c'%UXlΙRAO&ax-Yp<#Ky7ТMZJ׀κ6VJ"đgigk+'P*^|^"Μ;3p<*._>[_ ^}[xopAG}S.]=‡B'Pk9ػ?ǿ^}k7o X45sVZk4PV5ZK!dV(k(DLfy+I\L8"5ӹ,Kvo_g}8n(( g-JiUs^`p,Vڼ-!# x0ە65|BUg/~x b$8eY1-"%q Hԑ Hv#Q̫@kεl"! xA,Ѽ.3F)EA9ql>![ZAO%s?^ ZYSw I%ū]bNE!M4BJ ԍΧ7~^xspݗݝm7ꚷ|}3ozq5޽5: ̱Nl$R~] =v\XG$>/)O>uc[loo˯}Q8Ƣ~Ǥ(";TTZQ[G1<âfZ(StH WE~#Xƺý? LtMz S5eʹ()ʂ.lFYTjLue(ll:c:Q ʢԎb^"QNJzHB5?W{h~WHK'GTXkH~Z#2Xq%ꊪ b_q1JEAY11ƓQ#o(䋾8}A6SŬՓpc]-h|静c 0v`3^N1Ût1uwƭ!klnnēOw?m#` R`(Ȉ9@1I`TX\۟r87DQLp$F+F{(ٛANrH߸*1q 2;UoM"qwd荰Q)EmX*/2&ot \&:B-!dEnw,81 2PU52@, Y0R&loI:WmPJI|FE6h2[ƺoL ʲ qDGÉ =Κ$_ 8PqyOrlkup{𖷼ngI2H3IB|pf0KVa@NAH7x;q`exȿu>i~c2zŒHGM{D 58\S] g5^;\.a#K4p yd &uhe<)$obgg70qUhe>a ;?:4l0w.ҽ8( IE .RFiĖM ӺDB"C@_bq@H:VKYg)%:f锪 !F&i,-p A? ^[M ߛo*+Va"4 x9Ga.Ym[)fĉ|1E.\>k^V9(bR5Itn:vm'X7Ǻc jA>z5,N'#PK -.޿#?G!D<'}1+97og2ֈ 3'I @Jj5A& 6V.yht8!zCp+ )f< ?kko2c:$Y Y1:Q[߇$ㄸN=2=<`>![JoHTe 5g/]ߴBpY$HH02$9xO݌|#+$EvMnMG$DQb[< cݲ5 .%EP'L?6+JhQYw6<š&}!-W$!IRy#<w&>N?;( kKf!nk 6쌸~E̋!bD⅋z}VXV֬1Tu|>cnpMyS)7}j^xpM6~-D@^3<}^c:̳r鮋i  l7{tӌi1+v:o0M̦xM%%9KHfcn޸ dLsD1lnO=Ғ[nOp}wE8t:Y,qkgPf|q&.뮰}3^`wo18+)%cPN0DpH}ǸyK$O`a#BoUM-hTTH%!j50xgB0W8~Ex GA^ \ =8DR-Y l*;R ?c?AY:"|]qNUϘoaIi'ܼ9`oED_N׶߽88إ 1 [Lq:c[x xvF _zijh3~3򏲲[r zի>ꆅAR:G)Q*T^ 78UMkYB7QlWĚ^0覤FPSYF'OgHׯ}/oʎxWQ(O=.u)$ƅ9L&,' Ǐs͆? php%)5Ywn{}Do j7Y{;vɼ|^r$F"¦xz$IHG4QYj@|cSQUU],ΡT (5umgEh{-eŜAJu|go|UV c-ԂNIَ"Bs?Vg_ X •Tuskʘ=%pƌF{xsӏCQTUAYf58P1 L1;éYs]f:.+ͰQRc\4>M x'x ̦3._~.^D6RGI 2`GIsA]V'Vs4fD(097os;)`;ll?~$sFXk(˒յF/(*sL?w^R@<8Geeo`+NloϳpHIpDPڳ]覨X z}G9ٝ;။gxld4J ?!BS;^'ڣB(h`pTZ-̀j .(.Q[t^08qn/-eHW|ι:@ Ç +^A^- O"\8<ڋ7F\<յM[(u}37B:`]Mb1Y ׼a7OrLca2R9 pkdgϜfͬU;@BV~ᅴ^nOk/8 `=Å㫜_ 0ъ^'1xj8 AE}4i'y+ |1`[)eA}!WG`<)nN}ˇ~&N2L9m` RPϦu11Ls]'Os{S;cl› ^-IJ`G]dyFH@UUo௸nȑf)[m喆[ 3XkLB=0֑Jq̢E6x(WzdSk&bp%$eUQ`$QAJ,I__-9yG>F=f<`e=4_.Cu=XrC>k|O|#p8+"BDDDc B5Ex R4c\z`훞cV7RI駟]ܼy'ND h' #DÍ0JڢMZEu9oĘ4yjztI8l б˕^`:+yzŠ Ѫ{Գ!e9FPu'>p݃]._)@]FC^ ȺEk<K$s:Y!UU(G>Ʊm׶na@3L[WAy3p{&RAOP! ^٬!9M*V(Tpz/VWM&̦ O@ddF$ iSo3Y2Ƭol,$@@m-x\WJ[rμ6hZD@z :6u ~ $IE[lQཁƏ;{Ō+W_T%aVN-P9z IA8BE]xg٭Z[%seQCb 8:: qnf2$ rpx&OntR9fE-+b!kia, `*CYV=ѧw~Y:C\~W=FxN:c?AQ?ٓ'MvJC0زbks,ͱ޲Ήc'+!P2BɈ, = #vv$Ɂ 8 kxWQ֎8snx2guBuƅ5?&مQth)ak-EQ eYblHʲDiM'ψ"MK"aQJJ^E[ ƼBH?$B03X;,ZkTKS):RK0qA%)nWv1bm4;M\hoodx5;;c ke\0]ћsXc ʰuAs33t5yHzY0A8vxfA!= ~ٜ7wtRfrG (̎ @)1٬V Rd`+Bm=Jsiʸ lmoћdLfBI^xiΜ,1+8SO?l6c0Xmo}=OmJz B ;7"Eɵk/:Xc['1%5JhxPg EtNuHAH_xgkR'ή,+}8څKacM>T!!eyUՀhl=Q>WwɃs D>i-e[bG_m]=! 1EQ4R7i|EJEDqZf]K./JILc,#M2n>@7hf¼ޛ&/{Ř>IowټYOLmYVx :Zp&cuKd<'9I9akGGpppZSaQ)hsJ^H8\LӪR/CQŰJgctX+1ܳCoZiX P \3df!8"Iݽ=czyN%x/. EQ1̘Kt}J"!VZh FHZ}Сټ@Ɉsll0t]:0o8 ak`KEDom^(F5Q# - $Q;9u,OX`Pl0=m-_!f6/1i%O6-l\BuFQ$~]Y鑤)@P.ԫt=t:a`dRD;[55Y9IZ\ULS6}hjpٹX}RM0Q(NB0MB[)ʢLK 1RHj dj0*g<ce;AfTuM1/Wz _]/y+{Xc>};jt"/xdw(9I F~!|9Yl tDclB7?%΀3B,dD(I#$Q(N"Ț5唢) sx/e}m@'Q7U@+ . 8);$did:mјt6(%16yQR*ڢMZp{]ʪkn`U#*,Tg~D-B5LFNk8jdi Iܞp7͋`+liFxG&Usd|N g*pqw wA.FS<8I%,!!xII'%A Ec2gCkվ,1MX3󞺪Pg+G<)U#hcPKY̭1i$9"gV|C}1V]c9'(\n%Եq6o"MS^=|}$IFKm1 M-KHނ5@;:MНu|Ym^FjW&D*'Ww=AF S̫ɐlNeKXZc8:tHPԂ1{6 f81O.IuC o`hk+\fB1 :Ok4TI,Z0 +QmYx)yRDX{k`:58}uw|vָsúlhaxYb>k-Ά 8a@F-#1,>h{ JSW ;6,恅80Ug9cg8S +ҸjGm<Bc&Ւ}r5ÉxoSW{ٻ8srVm{.qmFZk_ȋ7n2NPsdXf~v$c_!:un|H}JG ,]cmp3.2cn_|x8"\S8'R;{Ed:e22ΘfeEi:, )IH:XxGh)+̿Sx IdIBcludHB0wMJW/-n,׳9 :;bk[SKp>T;/,Yv)rטfzjkB[1¡AI$kg`~|k4+q^4nwR7Eg+UA]׋t* KKzcY<vK}Nqk?b`kۧ2NQx2ɓ{NxFL!Ċ dʆ/bt24*(B)f)P65:ʪXծh&/WW@OjӴ:14UqChi2#БF8Urs_5 {:܍dllalc?QOUJ"gj}[58뗷'=>ք VšcN4N'DBd!2'h85Dzp((@@i95$#j(lV#}ia#A+j O*hƇ>(oX3/jv7cy^˲ԩܾ>JI<+O1!J |O# _`=2n1R8oMr' y7`kp8Jhr]qgNrkfb$"C]NnHE{e7$K3V/1:[%!`6+(~I`b Fz2(b8`.sf23 ~}Ǘp <|1 xs3ݪCLhi1!:ux$AlMCFD*AcL=7v?EQ!>9sa}spJ;s>n/$BBT`Z&07*ު(-!eY#diEH4=MSqIXX{m&/c,$i_)R\6=4y??§>)8&M7nI0R,QsW%Upkya:`^o~0d:rI"4k{eYr8.m>u2ј,Ce'([%2}|ţLⰎ8iCYA1K%@.E3pki :?t_!b>3O$ヘSk)eoR756 %A:#֒,UtӘNc&T#`k9΅ӛIN|}`{1U^ڇy'?8k+=_Gf4#M3ȢȈH;b (B(]vEΰ .jKUf>5%2ls14%:@pdoYYZg#.t*cլet /J cʗc,hZ&qB71V{m&-^.Ȁ5,eU:`k$;;;X^p9{$vr:.QmMls9{2c;xR΢VO#7ϳ;<౧E|9ɐ7apP٬d4:Cuɨse@,o rjctHngdL3B_3yǁ٨WFR;JJ&AjCYc(r11^ :n' mE6xA>ÙaaL&(nDlfYsbq"Z%-su*JiB+=Ͻ~u`IA:ֱ!%e]6OC;Ahzyy*hĉLY0E7!a_M6|eۧ-w  GIa~ym" $Mɲ pXvyTM)d4J _+"bSYpx멗6P2*(8m=QړBp8*6׺W{o=.`#dDx1y G#?}ŵk7U]t2cufskV;8=c2;aNQT\8{}߸MMt .a4wIIs=r2<0pV& k kڱsRՖ[wKݕe,e%Q̐Dc4/QMֶz%iw:ZVRI_t:BHjcܻu 6FڅE5 s 1-C'GH`KlPɧ_c={_'#5O?wyQ_9Mrt.c4|/܅б1&1TxTt,X)ˊ1++l`}u4(gB]܂kdr 0 !_",DfiB'c>eu]35H!;0Ʋ!ߍWmBo$YXc:&ILD:jZ%4\0ʇv2FߋS'y' 7o??zKSG|?<Q5kt.H>[[(!Ɖz6c.΂d>A;@83< mlÚҜ3V"hmV ɨpPAs7tA@ ]]k1G<?aԞƑx-rί~-Ow1B9elmn0;`22OM ĺ1f|E8I҄56Y(N>":4>:bߌZD/M Ut3͘NhO{ժ%H1n1`l7 $%a@My='ab818X ۲-ɲlْ*T}{5f?~k{reD89s^{5ooEY4 łmv=d_ k,}߱誮 Ο"Ĵ!Χ\e!d"fcJ2_c>{sJq=Ȯit}]ƍ[,|闼z9>>*R&&V~mOc\zd6jBҙؗ }G!\viL]`u֧kQc\|~dKoѶk[Ui9<s=g>'$'I1 SlVJfPh39B&Hq(+<+//~b_|37/_6E=RV;tM|-ۖvp!ňeRf:Rs6fU Jdp)*Y墟b6vFBkˉi>}OT%|m!Q$ r#Mv_ʋr8ƴ+nwcr UYП4]q6{{Z`bBpH|MG~+Wsz Ư*?n(E uPTe$mZXeq%Z[[ynmt Q`]1H@is ZO, {|ɬxŋٜM^5W ϰPXNCW~) %!F6<ߪ٬ JgC`aهa7z0g#1Qǻ0/GCδ?luIU•[/vE~74\8id׮#8?Y:s!Ϛ|Y7Wd'@c Qd+evΫ,3L$ 15uznpX6-m׮=$°:ω1R%! 0 UdQkBdUq԰"NJΟa]sQ&Q)38B6 17<1X,H1Q`[֙t6(ݝ ک(z<۶?w}&//g>|)2[[ϒtjP$E  :'*)jJk@i.pvBUl112lSTm;"sA` e<|sg1*چ$jhR敓ſ]~we(RIĘJ"O5 tZчb (RZڄ21 6w=/_.{G8|҅i:>-:Y.}?7o\o,O& Ƙ3;b-drh!+ah)[ {5J%vdJº!˦a\yX.=m۱lse ~ dL\uC s8k>|x16o[hVzXm׭w]ׯGY^ŠNԚy.x{}%ٔ3ooΏkXF=Cd`;PhDVF/"!ٞٚag.}gQT6p< ˎ2oš9>:gksc;2=cyOF&\ gt~x^[Wl.֖'5|¢8 )bY QP*c8g8[>+?͛{x|?,Ѻ\ d>9uWvmXXdD8C rR@f[[SU[yz'͝սst~bt`ww'^sGGsww8{fzV?wXdM^ܧjZ!9w<#\sgSvq۫!CȜAiCd4Ң *? @p<E6Ƨu×ϓR:VZ!#4ZF)ulfuINꚪ-lUZH7{d `J)&7"7?Xc/y;o9P&@iU-z>EU18!brdȅ9%u c,+Lglnp<잃%G\ [; ?ar9&I^sW^?Y״?ɧ_~C}f}Ozc+)K200UQ@)4].Nuʵ=3S_i?]Ο+r}eq9ܻˏock,O)E>ӸNwd=]MqPW4pQ"}? ?*Ͳv }杴I=@"FC@wmȍptt(Iv(d9b`V{#jzv뛱ZQ$ x1x`[v[{뿊Zڔ8ҜnUY)@CqU䕆"](zRjh'sxqѷh|&GdscFQw+|Z1ΙG>g|]HI) ,[89Ue޽rYJW7->D0ɀЇH`z5|+7\j|/7?XWU٭#>i;^xIn\{zg^ D^Gbvc0qу䮸^a)I΄§VZacbXl4t]Y2kێn0| Ӵ-1&ʲ [%bl&-9X6Ji;刱xC{W6f5ӊ 1=E|J֣Uc-κ5+r:XxYK `z+9&t}Xk_OY~"[T|TwhhrT& l$D&{k֙nPT[ ^}4O|?]:౷|E)K4<ЃlmPOfkSOW_¹X߱wuwl LBHʰ^sĘC\ G8IYwL" hA04GEKØkh{V[?6L/^wC|7~@R/Wlgf]#O=3<'|#eEjxȄdW.yl°M)qy/xCΜWPU??L W"*N)6/ #T(1^xYE\č)0cM,c챷m[ /Ц`hx8E=sWGO}ṕ|~ۿkvMˍ&E!ZRf/gaw,N|dL7kΝdgZ2): hZLKǹͭ򇾮r=s?vm_"?Kq._@ie:)Ӥ.ٝWڭ#O9Y,> =K=wCSVZՆ")*RO!d; տ|xxz};;E3ݨqFsxBR5!eE{}v{[/QZ>w0 Ңu94}`sR`.,Kvl׎B(J21"xgX\{R]ɹZ7u;Nz +-W_yW^d2}pez1(i{uv^pn.y U'֠%/@J*O>`ƲIuܾ} T_X54m ԙo^h/G#0JxզS 6"%F,y}vy3k )Yx])| YU)&a?:c=ٳ;|O=!uU3)'e =Q; Wƪ-F9tQ Y[*fvHlmA9ΟDf,-)%{ӛ8I]O^U擟lpϗǞ8^vʟ,h#7Pg׎>nIfE"ŤɄJc)\/ ! ]Hߚu|hѴ-OˡX~Wo~[c9>YƔ6$Xc6ɷ\ؙl4r69]qtOΙ lZb#ŀ1,o`};PqvH R$5mױpRm67f,K9qp `=0*o~wAMw||Fc3{+f=>ħ'u%)]쨖V"L34YKMJVkl~ń!o߭ERd)\܂GK>]ױ1㫾Ky Wpp2<{tlj+**{ڲ?V/GCYXn!^t'Z1)Acn]m|"gg|<#GO^2G { !|h%uX4YڜBO׵$RY"@4V IDֹ|by߫I=AlbհΎj)\o+yruO(˒qEy+uo6ǃfl5%E=0 `bIř3v9֎ MӒR˗tu]r/:r~Nj3W~(|E9ms1!%HYd$o8ڞ TzH:2>{ve!L؅?oGm+G>g1X5%QAK9͜bEcw~S:d,'ˠ4&^ҥ8wc ሱ%}Y͍#|,RNܥ(2I$U6>gugPNgʍA_[T |>ܸy|I..\6>n:dh-}/$7ԊrLg ;gjTln[!(ڦ#4s%67er7S%yOcԿy񕗾| |rhUyZ } ĤrzuR`6V*%R4[re!yt=0F/!"K*ct1uu_(Ik1$+>'QJ=i x*Cru[?)3o7nLЃ}鼙/x>9<^l{98+/>˳\t[$kGc_sNr 1D]O =}ȪkYszrH jGVZ|}m!? ==ЈAYcVAY]锢eITJt(UY'9RC~zރ[1XWXimXNs.7(ͅp<''ܼ|N߶*z~{a|Y.[ΓDMxc(+dZ⊼ZbCcp,ۛ3ʪeNKwx +EɋfHS$] Q^|p欂. } jn6B 9^jOzU0*_E:,h DNNN {G8kNP 1JDYr穫t+krdIYeE]s}+ iۖS)Fa9?iZ{.ϳ9Y,c]pܣ(cӺb ~Iۆӓ|Z|"6;hjmВR.}Cʸ=:j5h0n;`W)P5eQ6.,KNONYet2+nL0bIBveJ ̣T.]Ʉ(peR! nߺ~X}Ιo[6 W>C%]Hc$?YMH',%:*F+BS9ì90$$H64,ڞi@Q酳;(:1";h8:]ڰph{E(UQTI \oy}ovW}X4=!d !Wpù*ʪB.3Y4 IkqE؜^4?XlopQfb`v0L:GGάmRE1qYC uZ}Yk 1Q[e7{s9C=.d!dxeh ړ L"Fll6astZ1RWe| #Jnkݣ NO~/c,WSi>"p'O(Gxi]Ky˼AL-˶G$'F1--Ms VIؚФ,p#l‚$%hRJg(YnVr^=s,z-e4|S  Biտч.~Cvo;D|p60eFe)A+pZ M= 0ɲ;1J6 ſktNTَ |H!tEX{ xEB+=ט@+"Nw֘?9|lR}ͯY4ݯn:vl|`88eyO,}W<YgRbkk}w8&:JޫVQ?blFuWwuiD67[ ,eJ*J>DPLCYlegw6_u|!NN:ol[BL!)2 ΀uM'lL+`ZWB.b).Rc0bm<.ܻ.k{y Lhawww=q(f!H|BE/db[j.zX ~Hcr3:R6%ɖQU֚{$gdH6 !|Ώ{s[P+{(N;{?,OxEۯf{ezOa' FѠ 8=DԦ45)#EaHQ_:n+ Eä0Z/YfF:VՠB/ԃ":%cE$߫OVS߹-ȿJEaipeal\|;>D]8{h"R{+"\yR!)ɽ:16#~ȁ3,9̣]~,c ueQ@7TywR5WeO''4Ml:El,d(~"&܈H)Em!f_^;IDކ%wIVi999G~} 8&uI]|˖ G0k=pчa碴ia|5H}ZaJ&\c#qkmޫd}_6WO4?+Mh=:qEX5mtmyHky(&bٲ\64]S1>Bs_!G+E,4>l`yECn@Z/D$E QrC^9ly1bl@>no|;GOf1s+ѯN)9|,K|c*LZsA7}ߓEڕV?[΄Cc{OI\};D|a$@J}k,ܧ\Kf6f5IvrѴ_.#GV~gDϙgF CMϙ^I:[rc) 7HeJ!k4!114H@I~>&m\qv_#F {\x77Zi7'x~XvE\Y`6@U$ICQiX Ѳ+9WE{j+R ԓ: sN}1Ι@;%J D^m\|ϋ/MkmRB+w'Ƙ J&T6-ßFÐvH6YtzײYz&p!RY76;Su0+k#UdàZeD"$xghpcqk316w!q~ȇćDϹs<lnlw!7 {ޣZA[s|Jy]+ ;|blZsʢs|He2O!dá I}+Ӡ|d}X)ˊ)$RWGD7>b'A]Pp톐4ǏIUmCɼZtB`XS(3 -,AN8 Kkl !#V†3ڱ$`ĈqowCx|K(kwf'psjRWYOk5HrÝ0:+ ڶ(K.]~ܸ~ DRVzؽkEc;|I]|?uV)Κ{_xD.$$q1!DNȦGIc3(Nm x!b^Zgqy*KB,}qBZX)z?hrFLhCBK?G{ >"$!g_eos /ch+/=Y'l!fV.K BN# Ip9Ν=?ĵkWO|'__9\&YMBEEPs/({amA]UfR 1ggq+uQ|\٣P9cw)Ջ˄C =_@'eBREeyf>[JEfYQ,L8A<34si 0bCQEAk=Z?uvw׼AWQ8ػI۶9w9fc's1hguY bXL>rGgy򩧸z*_ַc4ܼyO{Q )lT$ZDPI Xy機ﰮ`6ޘ ŸU_K*\[Ӛ.FR15VB .d"!тւ"k|ϣzlN i!^8bJO*}NS#x#F {Z1O|׾ g-ϼx{v^/k8P!1YX:`BOW1F7޳CUAL)+ sFD`W8n\{'?anݺR*٘fŸ),pqk3Z!'56ZI{1ч5T .!@$ 19gX΍@*|DF<}iz*XpalG8O$9ɯ򷳹1Bݒ^¥pɄb&)EIp0 |k7*Q|`Aa'''CTev6)xX0*i9:g>W%IYGUlnL(ESDsJfQY6+韞UM MCZ8 7|raI>@ 14lTiF~Rq;8 1blF=2ߜfu~w88^`pwUi1blbNAҧtI+ 5U;OL&x͜R%;;ꊍ͜L,#aYVX8:g!,ɤb6-ȟ ̓ } [v6Xg 11o:D(B YuJamf Hc*%1SFT, 1blFSP/jߜ]od-~̋7Q8=٧`E666:dB!veQPWBA.wɄ|JcA$wׯ>ϭWX,j=ٜR{(J}΋I(3 1cB" ,5}?&aRR:K{Ĕ9ˬ5(z`N$ 1*E#AiCR=O̩MϦa/ˆƂBcOj\u/OS^a8:su666ek,&֕+@zuĭ1f7|6ԛ[Xk*Knݺp|p|VȶL'%UY)+J)I^SH綷؞̸CSVW}C|sٵχ>s {U2 MC9&8e5́I/C$2BfV(Q%?cp'Z [/uĈƻ%q/ g/?E^3/\kt}biҋXP(kΟ?ǙqEVzE$ hm`Ium۷nGXkT%Y°1a{c!*‹ug(8S@'Lvrf{6kƦ>ZZE!rX} buԵ } AGéS'!!Bg9/Q*`ʪ".nc-/^ňEDV8]:t; Ͻt^kϛlh.Nh\}٬߷8 uYFMkY.,sf x(ɤfw*k)~ٛyuM&6wMI H 67'AYUL=B9. b!&|h:Ç\ )}?}gtvCLB`$2H3D9P٭?W^~7\A@D1`}reKaxyYD1Nw:kV]>ќ+y-h;!=ODX.RZ$G1!kc.9>҃}p愍 ;3!;~/TmǀJDRGZ Uk%=S ۝99@sp<I}ǘ2$t>s >"9 Ԓ9MCB<7Ih ~7GIo]x.k-.?3mbO #abU;/s߰9 Mspc~!e]b4UF%! "+, ksRއLrt j9&8 QkAA(|! Q)ڙzAM3x#?˳O}o'<v.]¥>K1:9bG7 eaJofCBƇ19<]p|D@>Pm+9"DIDҿB Vu 74|n K6VVIʱ<(M]kitR"", !FLJȤ*VQ9k,$dǿlV+Bʑ$wPU]cAe-F[M )QnD2cpXc1J3VJ1|脯~}NL2)ܷ͝qR&_c0b/>{mކ^CEo\%|rιi]3k[bcRs'<4`R` 8LqrqL9 &:s ɤI:)R&ݢiZ>$EIC]R"V$}N /|uҠ r֚͝9Ε%]#`Ĉ7şOOFR;$8`m D**g/T笡*0AU[B>΅sh/y>'=ɡ \9esJ@gqoҷăٜyU_嫷h0#Q>9Z1z Z:HI]YmK$!ā;+yUhZ@LerbV1><c-7_z_G9yI!*',@i%a-b3gJ$e坼yVt0J E@a{:uІp&oX~IBb efAp)A@QVah RCvL)AW%I5s3c$i~ܳm=]"G,&L Q I !%Bw-XCU FM*XW\dÂ1%afBP|#F b6cВp#4۠^D;54X |٣|٣h0dB߰G!w!ȵ%m>^pvc-u=ɉJeXJgp Dk !b"1{&.^w絈$bs$IXdfYh}^rbkB58Hc9A5".-B16#>dbvbG %n(t6 H7_Aּ&Bm F'V$ B@RHc(%D7CR`0JcDBSC"EJa+$)W}$iԺk~H?ҵVZ5(Qa3B V#F _\ p=!%)*\ϙZOfG&]i+-DS$&VS*L+5x'R!7QYEץErH:_,i}D#8QĀi Ti=i6 lנx1blFki3e)Q8Y`7$ă۠pMЉRVJR$ It.1Ƶv^9 $8@])IN[v$eEBRIHLĤH*7xw?{1?+IgW)}<0b|2^әBH.*k>!4r9) 1 >Szi((Hrg5&+yl\]?o>~mXmł7}oF1NF&@UxVN{jPe 'kXC[W$A.d60o=m)ru."&s!7 ԥ*zyjjEO];g 87O:; c1blF' *hdf !TJᛆg&zq{a]"ַ+V}aeZlsϫ1fɹ1ü5z 0 V.z2EG!hEV6^[9FpM)PBE*Mi!Ni8|哴%ƹ5n!|bO -q- ܨ F*QG!цHqɾvI0f5A&w=ot/G qEh_Fw#F _y',&VMN81RNz2\hcB/X23k ^z/]E[Bv #L.ƫ>#(EKi51>FdId74$ ? Q.B)uԥL䵆EKIĈi@Gǵ(J' 4}.V})\*<LisxBHEޅ[gr*ן~ υAmG] B4]! gy 9¨\SV[$jx˓5Đ)a"jX2AizO[QAF EnRF16#x̀N]$4ʔ()L_m. l z^QZWI k-b崢ؤkv?W7Vi>>m~N˯VDJBm1xGʏ rhFC8SbEBJ @ATuby= J#d.B#!f_ЇD^ӷ_#`NZ)KI͟E( 6A 'sAH$&~ِRm02:GYNA ͵ gxeo% X!B:SN{Vz0>Pl ⰛYOb&D)s--ͺ@8D!% c#`= IR'i)a, Q> a6I!e@$b ֳ=[zh7_.@A (J1 ~ԄE88!'9 BrY6`*40`$iH!l\;jy3c1blF)ZѾ^V Ý(]!.'s$ x7BHPXcXUpĞ c$4DD)t#s~}&s!H)%Rxy3G ?XG#~e1 oOůAUJc1B$Κ$"xy:3d2Ǣ)h~24I1wXJݱt!_jqO?#`Ĉ_Yx`I d*ч $H5J:$@^H7$A^7E4 k/$&%K2?"~ 5['-y0#`Ĉ_rؐڛ3U(tzMSC4HvIc5c0 t!!(Bz0hNwEB#JZ O:>XG#~eqKsVb1S @1ɝIªHw2p֚H/0F?us1bĈ1 ܿ]a"~vun/[Kc.ȲJL$h ύ" H!t$yTACϷV- 81NI>8д{>}d,#F oL:8ZzoJ@EtSm/`Bas*<1DҽЙѯuks~& pWS(͗3潅hi~pLjc0b}L\9jY,{޴S`_`l.qv$yltdyX?b?fHJ`(GJ}fS"C(PJ)+ $gi[ >r4a#F čj fhxm4Ͼ|u=k]CDPN]6K*E;M I1)͉{;w7g/ר,{16#FܛzyI+_p4AH;|){a8*5'DY::0${5]e?s-$te{V&Ĭ*ȻUֈc0bĈ4>H4=k(Lf$]p8Ssc < !F:Jk:9m{ k2yP) CL+ @[&ՔcHIɲцZ#`Ĉ TZviB'R0Jp8^fcs`0f 1ЇH1;#`= y{JRH*5.5OnXgқtEҺ]Jߩ[~9 CJz=ֱ*-< <+0%-:/ۛۘʠK拆þ36Y.m ' %HӝǔIV#ae{kֆӓcR(("JhHP!!~_ssVrID^)ԝ>;DJse:Ik$^Uw9+]}I"K7XlN&ZRxs_yJTDrξ(3xXY+j-0fZJnԀ=]MoXC 5*hH kDŽrc q*J 1ܦ]ΙDSX?RuY{O=I7/N!b2Ii 1przJufԓ ]v`,RIW}X0LL'9==aX"цڙ|=c˜|ec1|NNO{4-)*B[PbKYM c-H)nўJ%JMch #KتbyGJcR61Q]ć"):6 Yr/)#4MC j.;bl^[CVgvvΔeydke-(0@+|P҆'"ciK;'zcl("$i9jqcL%K;@#}9=/֬ Rh;fK8FYޭ:c&7N??>VJN^V#v >DKU 1,mbL]G{p5QUUP$\.XQFfRXVPJpC!s-)I:3s{I]'?@vz8gPnlfsUhGmԄle]V0 mnR3R F.%7# [B-49;(7n y 6A)|=9%ň<,eI–M{ه*x16G{jh[7ߢ1<)y!4DaEJQ9&Ea#vzbpDkChrb G Ċn;nzh[5ߺ*w}69.V$lL'y`4t}@t}OvybmnRଥt$,#RY-8?H,I{98Y1߿{ͿkxohSO5abRCBbKz Y]h| ERc h,(!m?C=$I`~9t|b=9;mi(q{q$-s:$AP5طi x16C̏?MJXk:w4o>eRJ hmrq}ZN|n'Ǩ$%|1t!foa/mG\F>X1Fg֨$hg dh6Q*[ ץchN=Q>WD/=NHQ,N GG1 Ft~rԳzco;kP0=1/}K/h)łzRs|pDL W8ٌ$;t,NO.Zs1.VTQ2#c$s(]T|x16CFo@\J+lY )i ~b~OZ , GcB=L7R%I"20^SkСC<:UZSWػ{зC[崢4jZ~ݓL?+cm]]IH(J%CRPF RWdʣ}|MYk{qx̲8/*)Ѕ(CL{MGzrqOC+Պ̦̗K# F> u޹k5 m J0m30~@9E =L \fN()y[#1e[PJ3nqrx+'7xo}ӛ )]EJT9#-]H]}yڒLQڀ-@;2Ό1T>Eu%(!ӉC/(Kܼu#)D2hZBD#w2{jMi 5N{bhq岙)ETEtWpvyo]c ;6آСIhW"j3Pk+?6x@0[ Q9_d7=e铐%DElQN][H$ص/P`sQUQ).y |M5H#3F3 8$xI1C@DaL& (c{g7oEIr1&$iq1a]A =tsMHe1M/h  -1Ax!G;4jyfy“<,QiRYXE_gP;5$X6̪ n+ߡښvg ʢ^k"O{+(#HwK  "t9g""m̠!\еKшXW4BO\sBH\\E) .18 Pפ4u=T'h RYjDCD*耝3gPqJE6ON: >!o`#9D( PU𪱊1)<&)7nlpMRV(&$bFy-у-P"#1rV(0} I1Yb!e%.d'ĬO-FkL8qgXV4( X6MbhMst󔣜(S֔Մd96g9={8:_.=GQt9(="mZ|=`# aY(+@TW+8G;8=&czC=_΃_]Ū,}O9&KŖb#.e9CO6RSx =O t|Zo91EU|Yrrs<[/EkeU w$2e0";cH)蜛"s_rs} g D5jQHDѣG3^\S4}v2V I)rmEkolUPD&}qSkAGg;u>1sG9 Z"% 7wo|&%d w/Q.sCtJXcU(A.hS"NPѱG._{ N t vP> $R%ds*VZ6RH6[M߲H:vWeUwrə1a #Y9ED̦JŊHĚ%^A7uG bDtNVNr6 ޵1+6\Ղg2Gڐ\Bl`LRNb m H  z}] C$A`5zDL1f퐧FCC[r CtP !E]LI)jt9E6s\RDT"(AY\QB $ 5췚Cm|l4άvuiCkayz)`uϦl,iߝ#b}Vx9W1E6d X2J"T$ӑKQػ=."ZȩRm;hO5hѨs)%EL4RXm\3Ɉ8P٨nE<莒 rAIT)ݦ=N PU DYJQxUA4yBLX#"C5 0|>sأYc,\a|HTσVXkI(b))?C@a Oa@)P7w{4짉#'G!+[)xak^ʩ?8Q@^2TZE8dj歐%eRv"A<٢K lcPʂqL㎢lfFmv+7VCd_K8TCU ؊#3~e=Ư8'8Zq\s7wtnkR\Z6 w0},l'y+'DH]U\UN I0DnN!Voq1'8C XJy4;$((DQehj (1-ZRs09w HU1k qbOZGg!k)RF J1n3:S)9l(:kyr:ib &֦ QF2Els~ .Pڂ6hQPEPY2FrT\q&l Q:=X'ځZ$[blvDcASUE$`+T RZ IĶMֆ&\qT~@׌HRP5cKPZl%M(?}yp"6NhBfSH8Y)B{c>eFֹ+-(0FR;X g ߈`lX!^5*+^fl.Kҝ tcƩ&jO)-4 ?KЎ&u FJkEPkFT9c d:њ)$;1ݔSRX.(jʐtfmR(X<,n&q=1ev]#MZٍasRȒZXH-Yt:Y驥QNפ ;Y09i+IiA.% J$Ju6T(XGk'15۞!bs(9*E)lQɐrEW'c8`\%IIb[=[LSfp݊=Q1ѰTPa t|li_< '8>^>.hjN^MM?`[ՈB{p=EIð Q0Sc`DrloU]@E.Z5b;QNZC0>WԒVtEZjGa&ʑb7Pʩh^J1lN%ͳy59n޼}5/)b5tVhй%]spHY,QF+,Cw}:/.GV:PhY3JL UAumj6,ZjV-0AܣmIal7PRIl'T DdK^ ܀,97bmߠҔnJb!|,c IgtJ8kEZ$C JY/ &rx/ lT1#'7o [*K7E~,o)ѻa d,O[ZI9amSc^e=ۋĪZxZNoJBD/1YP%EAMO*; j*yڡ5fr+ C+֊JT[E2]/|@;ՁRZ ^)rbE<F+*9`,%WijABGϗ(s Pn` ":[ k,kL-- R~\8c QJr!V JtLL o5ZQ#Pd1nܸ)$nOH 1乏QTִLF74JĜ"35mɑUAc4w`j1]OU^ke 1M%t&n֌J݈)B&e.S$* FlFlI,RA1 IxjN%۫-}1 GcGX C))O.|X=ZZ?=6Z9Z6띄hn+{{ ?WGYG{V@-An$蜈)̙;c9nVB@F=8'CI0o@͔8bXKɈL]Q^pӞآu3J!)*)9xYkN:T%UjJ5f1Uf"ˍ7ǶtUYu^0{[ws&3>>jdl9 y7m-8]~rEۜ?z5̭ܾyg 1Mtg sϿ~{91#q~q7מ0Q9>ptrD?lJlzq؍c޸I|>s}r)X뚐 CSϮPA$ӣZP-X?`r)ŀFZŽ)HwJ?O]ag{7ެ;E9Bxtuק2cnEc`qZtZkJɔ,ߗsRȩrJ-E!VA:Oӈ6c 81|AUhCw]OAr1P:2<-cooW$jkޱv䦜K"Hck&DΉ_$qt]s%H#TREp6}C8+հ&6R !xk:ɱT:w1M($!29ή@eH_I+m=iƭjDY/uH9.gti*tG4b*pe;7߼˃s4<>s͚wtI1}ϑi x^=t0CޠcbgۑH)g;_$5Wֆg?ZB x'6\2%77?:AR Z67 W`֏A-Sb[E}O߭F"#=Wgv;(RȅQWVk4FM>My.t}]m@đ~7aBzb98D%R S""<n1l3 5K"vbP2/ ]^E/hEwRT!+*#]"1# >4Ip%go"ѥdfm]Rk Z9ǥUmH)Sh#J^P-]tMZAAG-P2g9%#fS.( u'a?|uAڞ5 9u$~{uJ98MZM(JrTlD֚"ؗ~#:Jܾ12)MIR 1-ct#HU")#=LrbRgLQ]+F&?VW \7-jyVLv=UUT/ԭ;kBڢv)aTa O`,ّH%,5O2IqX0pR"xҴ G8cUh#2#/ݞ҃pDAIQE%XD0~%EU\[P0qON9ﰾ) 1|~rrz3F)MJIqAHwjLK[ WPv41J\kίPRri׋8R 0-(mi0F,1 5TߗOh~p7>CpXiJa$8J䒹>XK-\bK+2'7ۙ5^2*(mb8Shzy^K#mAQS+rzAWv(e1-tDBѦ78Cn?W(R6tƴBi;WR Ves\@ǰ^c7 !6K= E 0MkR'b1 v”g0㜣*0dPI9Q)#Ŋ[Nf}4(bAgrrH[VuTPEj`8SQ1 AY"~vJ9ʅi %Ѝ|WT+C%4SCZD5FOCB;q#$&K#&n{m4ABSSGR"rSv4bu(% U Z ( NPXFjPd.J*.kq¸=MN @Y49ޏ8eY uL%qQF3m/Z=n^tт"ac9O(eڠEE)tPM7PKwbؑP4LԮUO%^_/zc.,'#v}R )GA pX]];\KU?}J~ R`!GکmʬW"+O|BzΧz!RJnaG0FJJOeھV~M^; FkrΤ,,B—>sΞGn[:%^P#XFs Q㜨 :nĦT(apEBPDWajSźb!+5!)DH*jʠȔ EF 3USw ޠFJg9*D-jJZ!(cJ'ʘړƎ.E8yOS@=Di/ cH.M) 5J3`lp EcB)^vF6eo֜u~CpXb;kw~m ȼJn<*XJnֈp)缸@_+վ;JA,yP K߯黮VSXӬU$Dg1;N1F RVy\MXkln!aRKaslnh~FdSKn޵+S.+/Ac(\bFiU B{mٸ5&^NBO22 !6{^k(p8 $c54Rw=C-S)ee!a=L4AcU{ymsʤHNaݓcj'D;ۀR8cF@?I\=(\Y& umNcy7gq%H)=|ˬOOG?& -'myGT;H5wV*ENY+ ?CXK)L$PZH)9RQĘ`_?ܑPw P2J\yA^tZx%%6qm24@ Zݕ6 Z6SNm&jfxŷ-aE9v=1FI2hlU~y6;`pi(+Uv$hlsi4Ֆ mlQ|7Cro'DD}rx#.XyT.1"'DS\H9EV1jaR)J-()JGn@BYҪ9 (ԺޑKiԿĬ[|艔2V:YǴv8NK8Ѫj5պMΈ;']ZR-fW+XwVچZX՚\qVccZ))"Sh=;ƩYE,;n[TyONWRU>Z+$,@>륖V+2iHjJ&vBra wuI"{21NrQJ`DE CaR51Mބ{\kmXiQN /Ίu5]׷tBiS[JaK3JNZ)lfsJ-ˋ3R)+T"}jg>e*y7x׹uzw^pt|"A)ŘBlg<͊.Dk i(P\w"H`o !FR7VvKgAką"W=VR^Cq&,} !ݱ9GZ SpfXhj }X+8/zҴf%;RD2s[>3rT (5vB-H{SYO9u 8QlLA "#0k(-^/iɥn ^"JbJy~?-7ZcjَDa89,Li;kRԖF&T{9kQx1J \t M%GJ/p`>Z7\}}ryau$RH9͑ h8JAY2)˜PZ_@hRzr+PqPDYKdB)"mR ZW-#3!fr#B r;5@ENEU%PU#Y"9^lujzbY h)k (c] ?ɠMkk:=7}k5W99` Zlep&sj4],pc jn`&0&]khuK,N4ZaY 2:P`,?L aX-オ>|F yv}.ve1{i#Pi$R xvVէA+1{2N@mڂeNu=LR׉:|X\۫@.uHZ+YznIe&Y˼6 |S+Ji|D^FK;&)Pbs(,knlVqڱ] F(RRhs?.tm48)U1[^`; Obg tK bJQuεП(U޵fuRcSl*ycܢVbL$3Z w^At"A_)INjqaH_cwΊ tĔ) D0ZDrڨe x_f[_ ~걤68O Wb?,U:#ӈzD9V@X!:3`q_^PL BB/ dB OoȆ\r=n% g>&VZ{Վ%|:cW,Fic8tc-Gg{6r'`V)eRIBb\f#bkJe#D8 ]K7AZr8gLjbISjGYgk'A\: X1MMfB{5V^\+q"K:f+*%mw]0\`RnGu[4Qr wb[ sNZL#9m{>pݷ\iɍyY)L_6\z䒉Ӟim= ZNra<c߬sZkϩw/ !bj_kIX;1bXyko8k$*Jg}RTmZwc ^BWs]p--M^%b|5X_c?z֡xW/G?~c4?K_}=:j`x{QM^s]Z&Sjn,Qr#N9QrZJ ]ۆ+\#aFTVvZh3pcj*HLGBc X W}1gXmLx#uZ^#iRHw2SVb!Fsxk. [NϮi*=I:{(Z epVldBc$TVNj8>Z#=@5$Slf!2ِsfX ,WZge$tj+b$H}ϰROu!޹FZ9z+c9Ra>P+~k4M]sN,h[6tC)Ezqoy\f,bX :bV6ec f]{?T+t 7Z3LK 8I #ARskp& Bo 㡡s΂L^ ΅a5Hb_X/ݒRck5}ᝥCQ 'Nu/QZP5WZuDj\5:cM\/ӷ\/KgDyG()\etX[o[fwZ;vLgoa;GUXƲZŐ~4N9Thmi6 i(]8=W6`%%b=C담|F,1 0JA_8^vhEh8guX]6mmmLk VN5^:LANVf~*k '7N8v,}i-qoYɦ,hkuI Y%pk0\QQR sJm| 7ms׾Η\Gć6į -,a.Jc$N~lYYٴҬY2.XPó^F9Z*LedSKQje%*aճ=FU4jeX"xC@1MQ}uWIM.TxأybN޾oN$dDElb IžVC+P,vO 2x%:DpRu(-{[zbYD3QkClZY/!&ѳ^@aKSǺMLWƭRb+h1j9R*\w?kl_c"DB7wyr!Bn * |+qΟ sbLe|V~YJ`W`YWRɈɴOTͅ3 UV\JclebӲ~׋{^M?puqRKB,U21{R<=-nwu|9w%wRNgb F+=1EZѦG[tӭH!`ݴr:mVLks5bW=n"B*@-:jN8+3bt, '>z qIh9??h#6Y6 'y/Թ^wizJ#) m-:R .EMn&2Djj z'a}1~?No*|)٬n+|mǭ=9%OONn02 7Ox'T^|v#k{7;w 3Mgo˼|` &̳w6tB䄘+>寿o?jt0 JK]ЊB 8e 9wBҤ6^1,㆔3SpTxKtHNl6u,N&+` &:Sc%dھxkΠ9 ik()㭕J4:g. A>C)1cYNV̺Gse5,3bŹd>ʤ}ȷwBd }cqvE_+)Xcn)) 4rt9]k6FVk)O o֢Ĩ月S}Z#:stYCFDX?{GwTo~󫩝TjOߦR;G΅)$}QvfH+bfcۜ_VMj3бݍz/^n7KxNˌ7?OVV$N;wvR)[(j>lV*fǔ"KXZoOKKM4jj Čw e6sn8 ]JjXЪ$nZM&L~!K-Y.cdבR$)!hᡕ䯍%sZ hyL|"?{T%U_? rg gow ΉJVf5TI{ ݬTє<{~"L2wx,y Cexg0\^?q^~_<=7pδ5Kd^kR[{W9en3c\r(#glj誨Ef4uwbh+b:o,r chƵˠ0x|~wN\my{oŰ91|G>'͎5 DD[5o6ljT7ZsaXpc|~fՊ~$r=::GK[e EhzIJ:~oqwmv"#W~Au2E-ؓ")f"b9 ks4c)׏9JUHdm k6gr*Ѫe6TX?)zZ42^>ThHNJVX=;B$fYir̋#۽^1TY../}9s?ja wymɧ4w忛'8$SKdFJ".oY3Le柲h]tţ7Jr)Rt]Ql"[Ħa(X 5jE߯Ɋ힓c֫k³Ϡxgȗ3Oq-G/Jm(5qs&Zb<r}o@7Tx˱YجV8!pM=>g&9^yU7cX \K pds870#ёey.vopK j OZ5N~Xk'q-|*ddZ,״`UVf@W)bL-LI-܆Ѧ X+zcyyZcz rwRĦm1K7Bātn&,@1 ~9999ۮrk1Cruz[Vkp7>C.]+P^Gؖyo"Vr2K!VD HlJsamw輜ӋL+fFqg əsZNMjZrƜXFb0}D8::+`C]ǴǗ/Δ"ª9Xκ/d oݐS(阨:M~)70ޑJ!&wj6Z_aI(@0ج`tVtF]Zx,?C.4=fνi]DQ7-oi!,?S5fZyj6ȟhPIrCc8PH!bbsadvl6 anT X SX{M]8ΐbs2h\g`b bru]/Z aQ@WU3Y Ý{@^x^VG=yﭱt]5Dm$9}ļib ȥs¥.RkF:D58C"ڹR %'|1Mr9%.?Z9qesY~m~o2q\Xf;Lk'9X-k $I4~'J@>}bn`ɕzy5p l\NO O. jɶpHOv毙sү,4,tj#t C?+6GG78ỞZ02 S4#n)qZJ ,Nc"Gcm (Z8륍5I!tiyNt֡8h;7lN鼣7 c{L#) $Q爍i`fTkyKr80{?w_NS)ER}O΂kZɜ,}غ ^,'v+ʏOc?ڳ]1 C߳)T> o&' ;w!c6]0E0S=|H;uۼ+m0u~|w;Fm1oB cޢp-.om^}O~~k][gycciiOڨzk[uu@e2o߻`/Hb8E]rS-{lv+ NH>U+DI=PzCu5M8[-URK5$T:㻞~`5^/{JC(&L97r֡x/֯6.užR49JAS mn+.T}?xܒBDRxP39 I۵yIS))QZ :uJJ"&㼻(v>syRH Rm#jb(ک88 ]@/Ѣ/ǎ\I)SjZMY§>A-xoe~Α\oE_) ?;^}_XC?}?Uk[!32-o{['}zj#}uG,}ҝ>gk?'>zRA$D :g)m3Hajo0J=אhRu,- "E+-Q!DR$x!hj뤕!_z)aEw`:}֡xO .,*Yf{̴ߢJOI{dsF"͊JY|m6\+≠l Ec D?K3EeRI)`]X+4o؝=(+RkJ:(;? ~ܺu ֛5\\\^oDd_+ݷ'~K5<7~_rkS[7︎s#'~]EӦ rJiӣ5:!Ή&J9I~iB(:3],PUTr fuN#_SD*շyL-%*I¨:܌PB摂)m`cL3#7/}h+(2)fȖltl?Ld5[._Sbntt1uJ19qo(+l?,}[>Gſ5g.H-g>e>H/>h c?^zKZ*?u!4˜pZA6yG{~ oPrl"&g$~91XF)۳{;an?')\bյFA#'-F +҄3詤0Mѯ7mE‰j ž0VNl]^:C'IOɿhgF9'_>p +1u[Gj|E,)X96Xj9zZzM~.Lw#)""qOv4ߝSrav%D(֖o9?o>fqפI9ͬEv_GIsK|gZɒu]!Jn%Y3EWM!&(Ii Gok"}1t]FS##\m ?Ο}ϼ??vCi_Ͽn|K!>|}M̄2m_% fZF<ac,4vVxRJ8@IS]JGQou9G!L@6rXJn߶77V').ytE)0]-J-}BH=Rssg% `a dT=*&r 䠫1B9Ʉ(@l>--93sJmEzM+Rc_lo8*~4ጣEI@#ٗHL7\nbZghMgc06T M2[C/AWc%ʠߛx[;%+6mHNnu~{?]^S?{?_|/x5gopy9曗p}o__ OlׇƴO]Po}/| EQUiRFR*)&,KA9Ҵ#N{RBk^"i]yͳ 4AJhD@X )EM)羝Ōs^~n)Bv$6FIl)PGS2{gJ+87+3I$X2(##4?imvRS^~߯d]}G-F mCԐ9'{@&\SeQd1qգ~BJ(1I[Q; +a؏YzD`{~8J s6-Cni:-.wsx!-e?8=;c qGLLqqo 1ƫw9Zu-5XUeV ij~1iW@Yw̧>~_?7ɿ]qvě|/1FcZ,|ZyB Βo(Sٞ^nI , %jQkvqZUJi8g-?߶{kҒYeJ+ˈ*58b'gJUi[Ip)8%hp&{u>ϣBN2c.J+Z *#>&rY.Enx@߹fk-z}Ǥ1ͷ8Gɉ8L{L# 9tJNXc`" (ܖ^xdݭ)9GNos"Hc N+ ءdg_y@vcWC\{‘h2ݼ9;5~y1<13NZ)U"g 8:Z=p?WR];xBo֮cΕC%s7?OWdl*-fĴVS9ҩ41Lu4E(:|, $Kk+R#lST)Ju^/Q Ւ0d%{`;Mc N)iKBɠ"1Q)[Oi˩j)LaXitt>`k'qп)R_IM ZVGrME h{u!$zq^R ]kQq0Zr✗`-7}mt2u a 0`[NFc7Q6#؏$ WyC̳B)Űq.at]R_/_r֚Tb ?}_9{|Aw|=ϧ>y[%>r@;n:BaBf-׈B&j9ς' *y3 t\ dSuV -2%.Q°B22jhmɵquZDP\|Ksh5?N<+_@~"_ziݾoxף{q> OY?Gnt%'KR XvmG9}7w٥Z2 3ЬV TiwW@ˡϙӞ&j\>~щXu=)%tv-K56*x+Ck ~t{~sx;<||ŃG%<cĺ^\\%U9Zw<-θyK ɕaxX/c"W}|8M8=x>7eZZ>;z#/|7o8=ϰW_xmsT^}ݿynj ^A X`QRߑɬ=ZeT_ $IUzۺ#ui_>­/gW?*^{?o~;}3X\ѣ 6|N_Rԥ$Oic[o=b{|,25e/sN-)/죒b7|pH7z5v5;߉v7IiΩe-R%/om5ҩjk]/sW@֕bLJnŇu(ރ)乤r-敵JoJ:\fXr"W䡷pɞ{cЧXj!ۖJi5M)1bnAM#y\ױ^am=T ԛb=?[ 7qh#G^L1o\]X/_^m4b&J~$s8m=Kn"=儺>ewS^EcZrD_䗾0Q`O{x_Xqy{o/*ޏ?ۉm}/lcOik$0zw~W:ITɀ5d09ب#~I,W{b@*ʴپ9uPU]Ь][I\/N"r[kYuX]^<-$9vzPj/nIajaE[R h$Ч*!VZS(֋ /ZQ60ˆo`iwG._͕ʴ1wAY]㺎GTVkك֍#đ#] `>7|E6Y9 ܧuL$(.Vߕ)DBaBCV{4qh)4"()6>0ބqw۷o{vy1-o'bLWoJw/ /WK1ý;r >*jI^?_c__7oq-'kN5x{|+WE8Im\1²4^N2TB"FAqSuΉb'UEhmQcX1K1:c{AkKi )ed|fqBF"aa u˲M%RRM{_} 3~U9'<8/W5Ut<1 uy\k:qDUAANVMcBDdXx| m fa cV-b~V.)):VҲTjP" Ma4춗-ac>Bџ3dq܌TN2D664%.׍jbq%҇a _cFOK1ͬY8M(hiMMK8KNEnF g Б}YTjjaidwUn Tn-C]*mm3FGT.ʆ7{bJyJEiC]߷ Pj5%u\#a ~d9zcH 2 oDn%4ahwxgȥryyj5Bq ׿^+~3z20 -k !8'ƔH!9Gkϋ8p}ˁD1t8N _k/x/`ٍ!;':}j)E}. ݎ ^\vӵ-!FޓTEbv1X38Ơf1Ѱ -dԡ2 ZXv{R>/>zyd2aY]т&wCi<.;{S&~U.lMqmÙ3_3hӛSfq`a4$u7޹Ɣ`ii+ʭZ Q/#7ams5f{&8%I)rtxbtluDkb tN)\`+r! >U_8D)3`xr\'%B_@9X]bO [Ș;=!(sI ]⽧YӮ:F:U+b!7,CR9GF*" k'#Pdafes{s۪zE&FOhzHbR߹3;-,ЊXV4}wIJhzUG;ʃ%,}ϭG+94n/Yz.B{˞Šъ&H1|'p嚼PWRH9̏+s\w_q}g^4Ʒ}4 &m]f]vɷٕ$<蚖{L6DAF/f}߿J8XzV%E~TU)#D}ӸYeAUMhA)c.D@*KGYy+G>/Ӷ9(KB̻p:N _ @Kw5@X' weIwb" ELw2ybX.@]WxX%mۣp<$L& Qm2NX96E'uȍj3$(R~?wqXppmKP"jDUXey˨bK1Ll0c:R#)LDYB \Ӎ:W`o}pi߳sϜ9D\xDk’bS u]Z-20 yy=܇馱x/9oPܹo_<4FD `ՀI9^Ľ{|{|o{VB#t=PD&HZlFA6l ʒ h}b1_5=']a'uR|_7qKQ:ٮisPatmGnRtOr`x{Qe ]OL& ޹|bXjR4!F,cpϻc=!șY׶/m⺾)4DQ͸rpyNlsmym\xj1"ghxLSwزJ!RUtmK l:lmo4-eYQmP%ZYtEEY@Da>HQC1fdHG{Ksw{|j1=ͷrRg˜ɉL߈"PUO8{s޿GL_|z2sf$t$-ՊmX.5*Ь:zؚgqt$R籅]5,,4&|w\ fറloop}>?o''I'Uӱ{0ͤ(hRm4I=k*D|[mYXBtR1($&!vmBlLkMsYt2+EkR!{X,ƁNEY`mA]OLg6ܞq"_`ck; K M|ѵ Wg>2?~//7X̗ܹqw1Q ? ^)\=!&R,A\wb`Xr66)0kQcj;_)R/:GzL 4`PQGá] JibL\>'YRR(Q !E3\FOyo_ W.==w}6RR\|c48yCv8d2 u!51J7_e^u%\) ee4Ln޾/dros* :糋q|"ۿK<ΔOwpɍ:)~U:!UˤZmrֻkk\9/A,CF֊7Nڈi|,Wb!94h1],vmݘm0Ψ&)lnl)rC=eSV%Mn:y?Y3M9. ށ6Tu!NJQ ?p77N1۸DHmDlX,Y.0j^xy^xy}w#1`Ԙr'd Vڢ z)pѮVK6 >V9aח8&emIU+Y!@b)H|~F1s(KRbtU~/_'7U)ү8d mo$!a+Cc'Jk 5h[-bc:!Dp"3eEQlmn)wmn1f22L'\/Q>xeX\.ARH!RFj*q݇{P lUqkl|y ao#l=9֊2yљUPV%f1( 3ԲsnV}|zAZCʞ%d>'()9Nx/|n؟> {2.5rnし"&kpO> /=BAp]sVh;HE)tJ=L2p<UQyJʊ,1di&әlNjcstp@6Di+)\OD.E ?+%{h?7 1C`l( lJB S؏;\'w3HJ!0T9UeYQJc1)uGkl6؊FY6|gΞc)lt6ʓy ;!{[Ym;yt)wZxH>[e:k% ^lo%fw( N=CQs5x'k#m+y>(+r%nr%){Gb4JEB5ktНc2-0e>vGPL%/t9D䚴>8/?+0-K\]9r>z 3hI¦-s$GmO@'_CEQ`lA983qeX-8?Z꺢ZՊnb>_H (r>/eڦt]/dB&yO'Iq:rĔ˂KQ5SʢD9kxJ)*VM .qL66Ͷmn0̘L'EN#x9B]LIKKuvUc/F+B|?!C\MAut&JiVMBHg黎.;677>EbI׵$8;{;?8䑫)_MI3gNce1_X*y|F$kI71 e :E|H &#oՁm:~z᠕?vm~g?}OP;}m: =˃=v2̟ib|Br(̱iFC_f42UCU4F "(EQOlls,eYF-ﻖbj>ਬlVCRoYhTU)[ˆ%JzJ~O8Aj(aRWll8SrkCQʲV(v}U]o` sV9F)ʰZ8zPFPW Ν_8;Ar(pUk%7ʁhHdKIvZ J0IIѶ2QߗܖRJ#[lldiZӻHb#gN)6fՎ;*rM˶IrSqoYO$Ri]'aP؂Ϟ!]ۡ.BFs=H$jf(<1%l~.=G!<"yǻ|R0Cw3늺ަϰ}3\I6iVf@s!N`=b>a*_8I!~swNNy#G%}N`Pʯ.c4}SW4 PhZ J[,I 9.˂.[85KY1e KYYh񍬸;ƿ𱿏ٟ>9LN O,Q򼿻QJL˛Mjfc_a .(mIi VB (ZHps.~-ܡ+$ŵmfX 9Tc4r,jB+c{RG&HR kXS}$=!B1xڑ$hgRfS67Aêf0NFln k˂tʙgv*ptxDjJ@G)1,W+剃-<-)(lԹEas$oB%Q4X o(T\fQ;;;<ԓ|ҋ1KvC޹ϩD*1'l!W׍ )^"y!UJUΈϝVZh}(60b,iTuʝT} QpYX jٱ1X-{ͥܡ 0ac+6YecNj,啒)9Tߣ/\<Dž>6 mҬg6 Q0_kʲ=F+T] (fD/Ua(&Đ9;&]sOCA3٧0ZaqO5ƇIIL$@)Fs#?:?@uڎh输,?S^ϕ)ѹ85AdADT1nj2!J)9$bIê$b f[|p[ӵ-0%$SLE1Frk) oW93ÐtM]XVF8&-\'I]BbgZOyj10$Ep }^ CK"½7Z-*UZcǮ]ז[Mbo93SRLtAlrc)5CJ(O?h0QI1#HLIB%EYbEZhJuab T<>keUR%yM {rkmv.c^ǃ9jѣj3Z #kj8<'?+^?z,,4K)PUlЌ1IѾ1N  ];j1aLc^›X <b% b1^Y~/?+`ELƷZ3H'hLa⠍ߎ!1Nwt6'BkY f~8a.\ۍP'et^i ͇EX=_ӓ8N.ѕR8mVT[v^RjJJZ"̖&kӲ '7XHxIQR)ͪum k+]y䆗G2c} q8ll0Z Ҹ}j ) h8В9 H&?&R$Dߵhm0CE!ģn;mљƼoNd U];5fK`&'Y6(kżQ)R(2j!R_clmL'f`Mٱ+2S P?hzyur2Enu |sEqAL\^F621 : 45}1l뀄BevȈܔ{PXXY<^|Z꺒:Svd$ч*IŏA1 iK8̘ٹd$*!,Y,xSL*RޛI1 +e0\<5*+t!"M0ZS3M@BE w;~zoٔ g|#*#k W">&e f[s#*AY<ה|r6㫽Rp*ѠAXLθe{ˬx.X-PlYTeILкtM#ilFa]?}HR]xy_dǂs^& y< j@QBƛܹ{"gOb6RʈYEB|lldKY$9<F}'5$Zo[g:b699v;Fw8(+sjPDzȰ{ }FN+ZIg(SrlH#랁8<$,RByNk & hIREԶ.=޵M%hcFm0 !D$Fb&ac"6FNn>!s{Vo^S׮BugMOmSN6Pn'u挺s4_R!杭(۱:VOGMv=֚\h0vuJ+L2XhVXkq\1u^xmn~@uܻ!qΝ=MpW+فF{1r؀q"Ƙ&y!CKS~ r&v [@ iɶRDeF?s .с.|ZCYr1E!(;Qe-2&XK^k E5h/p|>Ļr6i^z6~(b*wi1Tf<>{ 軆i]ßNmo[2VV !,R, "V|dndES k FBeFls=?m.))$_h1Ռحm=^{b6) !ep>gE!JNCr]j}k rMg{t M۲ɏyokTu[zv Y l ;$Pa<CY)EPX\v 4>[qb5H{BשHvT(D*BEm--Ɍc?GZdH8\'w3/tF}t>1 &gز[m_Dױ8uK zR(\]I UXA)z eDߵkܔc$ WXN(~y ]IDў7P݂ܹ}K (q{gCfޏ]9Mre1kP@̪c"󎾏 I[ҝRN0m$>imURKcZ+L)_+2IFC|BV !DjC)EaMgT~-IR,́?-$'"r{>J)1Pf]BRT?qHS*I\[PS/S>=R-)Y$]RB$I8LCIA1֌5+!8%"BE64MZd60uB|2]K{}:iʈ*{r\'wX>nwiءR-:ߥJt~oHJF_%V `lRpaس&' BQ(Dm]3ww^ul}S ~'FyoaZO2I*IV+yybC&"-o>E탈Kq?gV;sZ&#rU!whPm|$eBנ]N }K,'!S 񱇍S/з(SjHBCz"mA0Ru=+rjg(PJt d&F{EBژ^zbtk@k+t]7EBھ*K22S8!jm=G&Q]fB+Fi)N(%k.Z%_~=]]bu,Z lŤFszkFҖ Ο=ͅJ`qX\']΄ݏcC᮫8;E N+L0Z: z)'jBXL[|<^hW:fbw!1: IC $RL]e=0hƼvowJ #,:5FBVpCLi8pTy0XL>ƭs>t:0! JIRmAQ16ўhJ:=pJi[OPWKB1 jF"%Ioj $y7zAǀ.4mFBp$K!\b={;zIyDG 15S|D$kMu있1ORY!JӀtxX)s2b5"9."Y270ŋ)B_~e JKTKhE 7KbHzY< y ڏ:)ۗ6ۇ.8[&SW5A%* r+t=%K-E1E:lhqk}{bIEU 锋.p9Nڡ+e ³|pkPΣvWˎmUh3ly1N}ڢ*M+ r+9'[qE,2 I}@rb(]O =M)M 67GScL-rgP_GT(_Gb-*(b@2 `<0be1z0Qj$'9}`#}ր;1EI;̄)ʂta렔Qʊֳ/1*H}YGl9URk"rEPTW$A5;[xe1fY C!3 1 :l9!2-(P)9pQĀk:)N֓]||no :yz/Pg6݆K(hl)$usЉrkBEGUp/ƷsuYOGrxyY-\8?>ƴi8TuE̶2LÚXBsUUO@,9=#' 0 ,򤆱T"]Hc#I}'Ͳ*A u?`<|[ooŭ?Dk+|W6eKJT$=ݠY!L]c TNE64-;(Λhs@~21Ȁ|!tȁ#U!-T#Az^uNǵ2MbԌ1$bTW-]~|ͮ 3\|1rxx۷y7*CljtqJ p t0۠zm,Q jtb+ы]TdN]},عzKZr!He-4УRb;̲JBAWJ9?Y^y\t mcN\|~[w/xͩ9: d!퇛v>X!ϝV۶#DU{W 89Aֆ1Ɗ%Gи6,28wJJaZA }^k+U OA`TbV)QjkӮ6 cI)Ŭi霅Ɏ*F[Rfgk+;N&@ Tgs풮9]nbLR *I_)X,63Q:h)P z28?~(9)>W]4t0 bԥ^ḟ)&m𪦏`mJb-kVT: \Cdcq$+EǨLeK'>9]1+%Ѕ"<9 @a|CO.\Bˆ!fzHpEa@1UR%pX5H Ş@ e ز`*'H R"D6*Pјc n `wy00E,'**Y1 !xȸ]'( } # K;,Swspb<Rmx,IJ31ecJIFMHY<)܅4F>51%Wc7>K =HQ2![?VJ9 Yhs:bۧ[úk ޶7aq\HUM 4&(4QɜH!$8XtٝSd9v֕Z;; SȜ[Xl1$y;E"ϠdL2FCa5ƈN I0%8("pԢBuKi0sUM4b`4$c\3tΡR$1b#{O3tơ"Ij=O)Ҋ!S @c`Ppy }ҴPJ)t\#yG-JΖkC)O(eG߾66~#zap z7զ@ B.`:R%mΟhMW $#ugSpr}dEuZ\{,åDeA]ҁǞzgS5)i55hV;z%~?+ ׭>:<&:A!=Fٽ%ٯgլ>`2͢(06wsq+WJlarNZ;oږ3_}{t nC0{umKu ʚ]Lc{돬Od!C"S P;rЦ7vC$ZѷHhN<)"e!RJ3O+˹kw|Iz!XY9f@ >J"cw# :zF 1cv !Qt8pq䃻l*m&%h)Dq){6|uMa-E{rp§?ʏ?ϳETw7pS#=bJ]{a2̧,K^$37q9jvޯ] Zkʲ@"$_i7_,ƌ8ïag읧;\ǐ )1*ȓa,a$z'/0e>佨ƕ&(j`4|OpҨI뚼ZtTsüחUۘf@J~ @CvGޱxKDLRX)&% "p}h /}5:.hs$_UEQcRP m(veueypp|~^Fx Soc.YL1[Q_@aK?Xao@OpPD9)i{N'+W~ gФBOKy7}'7޹EUלޘ0h{jTD,4\mE]Լ;苯Ǟ{oU||he 7# 'C$*7L)EQCYBO!9"sc/}biZ>uLX̙f e`!eJ TɁ4ZGaδhapKΰWd+RܓVC]mKP:ptmPy‘ }\j-Q%Jaڴ-MwmInK۴ɘR>c)7P.Mr", `R|W=F+xao̧[/mW?歿ur>)N_ɫ"&A}LC&M^ily5h (2!&͇Ogo~zțośoɽ{XV2f, c2pi]Nzd!ֲƻ{)F)-;\Ub >f_}zY5-m96zڮ萃.^8>J*(~uy|l,ol%dZn.sI)\5!A xM!&)K=VBI|>%t#7b$iKL΃J+e }.?o81ѵ3*o01H&Av+'kp}dWJ@fPq-1kQdG"Slډ &>|_q,p۶yrb16 ;;;\xSNq6޿օ\cտFݠP"-l%ѯ)e#[},V9~3LM4<|G3WmUg9=ΟO " ;O(h y_ek-N L #(TQYtQb *(tbܽsMoc)*W_}ãCcu>x͛7ywxկSOo%(X&&wi9E^3Iomr!snj{aVZtsjIu,svwwFuY5!pt$[_JqUL)ʢ r8}T5>g1)hl.jkA'1Ew>J[Hdo)&FB N DJ= ]%PdRs4_bw l4#點r|`lX98<9/.+[TeIn;q% .KLev  3à;YKc5ĘPkDsBp֭[ܹu?{oSO`Zl|}n(СE LzZ$ #z&uiI@]>/~>r'>Aqxs?=Q'SumW]<g~l47xw{JKZ{%[uIK||)(PI2ʣo؏C\} )z|=ז9?6._ŋ㡻\,`2uEsv6PڠX|4MKӴ,WqG,Ww";AѼ1qt}S ! xG)JKEYTeUY2Nhnz%劾S%Jr*ʢu J+T>׊mmeZUxSʪDU ]KT:4tVyډ>$BJgG|^aVJc`$VMV ͊rE=}pY8DʪKwαpkK#6b-mĕbzF`2eDq*S@׺1@8fK(@)0ZcS? ś N1]8,._摫Wiz2aXROSO=1E3lƭ/n=>v4OQyA'޻{Huts­{+zQ)ؾ85~*̟;\pDxR\>elӓӗy{~3w[l=4_yK&;SٚXc!Y*cy9u,^`~tdޣ^g}ߏ¨a6yKhxore/GfZ}0d=sU+hV }@%꺖C,0JZdaٷmGU>[zlaM.T²,N'ћiZb0f7E/ $7{VM*EhBZcVZSeVk|2LFdkC Fiʪ*+6PMGEQ YA,&(eEr=!iL9 bB"IP} v iާ,h;G#rAyX,XIASw1%*[õm;/ ڋmc ^x>/_ eQ{~O0M}>];ڇ # C];aAy(֌j>4 [Ηh n!ar"Z-쨱Xc6KQ}S9ď`y .PZѪLJ՚o0xUQS>)>)^w_i? 'w-?IYPX9|Rhš?C-W\g9s IM==O%Gț(۳IuL {-V¸#\bnpj2e:2`k P1c}YTUh 믿{4';J>w≪zǤ*'517768u[[;)UUR5;9u_xHiۖ[aT'G~OCȵk(;8#Ws{r$F*2M|@QcXBf@\U6x%RM+ ɔ}]@YʉuA I@n%6bJ"TAzTjm )Q[_,.J(CL?`}HaDQV.)%&,(~"[.a29}4^'x3OGl,\~_|/fgPeYHQ8tJT1ywv眬_iVt!{ [ 2Uv*sƄOs?U]Z.G61#Ycŀ-H0M=T5);,c ˾X#.Di5_8{ٙllR?͙~?_oMYyr'oP }c)Paa( [Xk98<[`{ٜ}a3aM+l2)Nk;\0Vlm:ٲsYʍgp3y_;LVˆbccJr {Gu977KPUd2lln6(EJѵ-Jk5`59v%޿EӶPV%gOܹsC ׻KuG_R%f$JFY:<O\<_6[Oz,Tא&x'IM"-.ɮ_RG |?b4]X]ʃJUaXŴ_w:%lOZkaxODyBdBYE!hdccK/cO#WtJA4 ~5[ܾs|1gF%(@G)&)t5#H:Bɮiт|.,d"{bk6'&5˦F((wv0Q5u]+J)VfKE!vפ(m4D>Pk4M/J޸ɽ9)(y"=X)/z-;'ׯ@Jrb*>x+ TLQ9.]_:mӐJCWm[\=;eӲuJPSe-lD_n6wZ=lVY!g kj%?/36ml2m0`csنt""Ouc.mӌղh.}O!x#(Q:8bDתo9 Ɏ0jt?*QtmDzr4-mu0{b8mޑ ":[caTS0+)4,ږen}cpG2)N T'iELy'E0 C"JFB,J(Jc,yQRW We.kI6W^ի\|rWύh;R[D6BKbi1X DZ¥BY-QеsOQLgln*3Bt0Rr]Iނm9Eg>`d2+676Q- d1 )e8]&ѫ531R섘h{N->tMs? ӻ@;mg秹pj/+bw]'IprZJzwsfsB߬خbw*H b8 )#>q4:Ǎ[ (谄< xa9߆ܮ12NI˥X2o)ل-Q\y]/mזkpA'wu͍℈Ҍ&maJ+ًgc5REMt}8;*&QðlZڶgjQ.}푔]Ѷ ]׳lZj¨1«I u9%m*wY`YP=6$KU ]>ØjcKL#W62TUM]*5L"mߣ9<>()Jw}GB~FX1qex>9S&왳?wrGۥPJ16Fi:U{띯7l"f‹/1kou.?srS=)NA5?1)SZ3;oRĩ%/(.jfV޿o1~eٞ<8l)mw0ڢe([40UOr|>mۜ;NFw⋼+\|9 r^{5/62AYWpZNZrBSVTV,*M>Xm{Y$QNLLXcH{pLOF1ζٴmiw4_жHZPU⺀7PJF=BΡ StQq`Iy emhCIq~J:Ag]th{䅂X._µkxǸt|"Ȥ,VozO96 ]M5!ˬJECD1ߴA{CޛorO?ȧ?iy-}vgy}s;xgpЩ:cdKQWY`s!, la(J;lz|G/:QnUff/Gi+ E)wpgjYQH7UAL=ɷcwca Ɩԥƃg|&Q|cgxT,eܲ"E$bU6DDVɿkp\̵k׸v*;ۛX]]"U)!ǴZB"tIm )Q:xZc ӈL, vggo2?51L&v0FS_|sO=eYr W\ᥗ^k_/Kz7hY,yj0ɴ].><֣Psb`ɽx`tf99;6&|wvV}o~ȀY O:v/VX+JE Jkރw!Rq~Ss*k<}&Xppf]pzF+paBЉU0lj}ϲi$ fՌs(!W.?… L&FU7fuͅ qNS iV+5? JCBD}+J{G3 DGGcd0{ZF !hĿF5ȫ)Nz2Rf p99V;-auj2Z|m$/8&Hkc1=ZU/^m[~]^xcɘ<9ھ veJ p]J!+ygNO=\[C"u-b{SOM:\7)x֙QcZ7bJ}8)N8`}Q?Y(P}bN5"_җa1~rfkQsTVJ ׵ΜL1ܟi.w2tVy59>gQq$#%] &4?v:yL/7laHidUӊPhy4J~ 2)0ȧD#i=z,*hMrW W_9 #,ݥB4!K@CI5(EaF-JT>Rt@(`./pu6UaH!zC '(oT\)>4{˖Gc~$!1>g;BZCRD"]J=.+%!СX=vơx6;~nd2C>g0#b. z'zɄ"I)Ϋ5) ?Y)SN߁4L ZG^|E=vdN7o2Yl ]SN#95yg攮_Ν{\z Y-=[[[bޒ[-:xAO>PJS]|/AXRŸ-tw8U=pjʬ.qu1u͗eG EKr4z2!+Tt(qq JGg\rY18.z&%CBȴ<~ͶOCs47ek )㝙2(R| ˄!+~H hs# 8j nP=f 9t&2/.%!{B/ M:4UzJDQSGxu cF/)\[lVز{bF~J"gw_}xxu]׾ A&q!rG#W3FATQ9(7Yd#$h:<ˊj]=k(Z -kmwblՀ”CgXC "@R%ca|t#}14ӚT(;Ô5!R=Ĉֲ{W@Yh$DWkAט&vͨPcbBEu1P@X(U !LX,l:cRu)!' L%lY$FU,K v RK]QܙaA0T~;LH:^?>A#0DS%stt|A{= F돠*14<?_xÑ6xwI]pGCy'?I?ȴP1k?_?G>ăh֫IprEgnUYZ)?!i]=+٨K/ҭ7|ΗV9S\y:oUAEG" ynrm{:O<޾޾@xè8hwEC7u]8iGFYE o+bY Im+LccF${gSV&3 쇣!a04i8P8܆1a8t.J;",7%*(%J1IKHZ =F[!FQ3~)4( d , gBJ<%-YX.2%!aYbՊ>Rk[uU~)=yEAҊ#mw=uUʯAҦxI?,-džHa=%xbVAC;!!pw[nj=5!DJ1)tKp5}O|隣]G?k_k-k-٨XU;⏾ MGv9N +9~dտM o~2\>E`Q:G]S'/n ( 0bJ1]1:[ _{Ϟӟ^˅$Ĝf)>D8rn,J'+b=*42GUU=*Jǜy,l!ok:oӣH ;ؘ"o,M)Py63b^x?ijR()UƻPFGа1O̰Xדo1%w!y=2:vP&m0VaZnT .XΖS\ߡRB\i(JT y 60N׏9?ػãȤ<7<~;'_ks}۴.፛UkWIyb#@BBK9[ۛ(2\G]i'BhE2!Dz7< ă`xEkr#nã9?w?: DR0$:v*oJ)\h𹭊w~].]$łm^x:2VqԌا'.^¿r!xr'ׯإP/N_3??w(Zv6*#$y .3x.˒|+Pd4#? Ӑ(ht%Ūjw[[|3?s$Uܻwpxx8mq萇)9NNKk =ɣ!8BݱIH|*zb2p}?&Dt:'#bX_'+u.]z~7?{OY샏!UX3v y1OeԸ.g%>P)0QbCeJ|}}6"<6J)IR>|u>\ߦ ∤,(PGV\3=+z .LyxSw.2kqf)%\xGp7oޔϡԥW\;uiI&ʬ(Uj~r>)N__S/)O|n[PVd* M 27n>޽ܺ}\}@t#;wz I5U’q\yM+B34i)&a@#ݙkj8/3O?3g{{3gϲ;vu)%ZΙ֎"*JcCQƐBY(C GE!6yXrQ]Nc-ʂʣ2ߐz0eIBf ,*|5-G!q!aAZ"m:6f3@?Khc{w?A~FҸMcgC3cA@|Dq2 (F! 商8lYr,/Eܽ*ò\'sG(ʂsgvÇܸ!o>\ْ2mblI:6:60vlg~r묣ƍ:}skN͛(؞U,Ii93© ^x_}o3+ϝܥO 뗡Q"Zc~G ӝs|/ƌE4XΝrp'?=Xkj/|;Z)lT/}woB4DTmJRr j+~_<rg)˒'|Cԓ jɃܸq7x|KزJϻʲ,2{F(F*q8!"ZohM;~A=Pr s8 ޿ G1Ԉ5JC?SZ1qdMX`} p H:1Dq |蘰eEa >{Q~z%Fڴt&GBB'? SFڲ_bDH]4y)%9l}VOK;rSB֪^&%BYa-UUWݱ)yq\k"Ayλ0֌k51Aqt^:;3LǜQID/^o͟fsc'^|Oy667ۖwm{&k\r+oK[eQJ6 U=w7 )R;b,%ijO>&Bzk, q#X=|{^FJpfuщy޻Dzl]zO\9;{ÿur>)WZ)R (U[3 >swjFAUU8gB[%ij%ɔs t2Ȉ|uE>whR8 :em yuP13)%kKPJ<5*1(5$!aIwBEQt=Ep>?D= bJt91Xgag_s8tCB=b\ Q!J5λH:QhTJnQs@B[$imn1;{noUelQL"$$HHQ>@"G@Q)"N M쪺n{7kv6cs "ҖN\s9xbMܭx!!9nViCN4o~7^RCh.o߽Ï~]Jμ8>ރ\,/rϼswK.>Bۆv2#=%L$Rpଗ vo\ֵ˼89j),8Xszz?Gv?0i,j~Y'W awwk~~񇥮U.V#W?^咁IG}]_y?_amg|ŗL'e9V==w{Y,[/~ W\ G& 8׊x)Gu=iTߙܙg~ĭWꋏ8!}zp#Ĥ54:ZKBdJTT)0^ƲQƻrC$b0]jۙ%BNr-T&6ztGa6PJو mఉDn}HBb( R҈q @ȍY"GFLUk'A;*zʲAvaɨůe qEд ] Ѯlm3جYj;^Dh*:6IuSUWfZకʸ)(]hM)" >lt(MFDh#dqܪ2D(dG6 pE#u=c޽:獽vxS0 F2 a1ΰ/9̻ogl1X r~r_oƏ8=;G5-\>l>F)N';7vf"kѕ__]/GՊw?㧟>}u z5 Br)7o_wxO?>ޔwora#Sٟ7,[oeowB5wчCrdX^}mB5=aV8b"!M(Џ M8=%lݤu)JWIqlC6a8m;ڦVp#~kA G[0vؚ6ޠ(@U~6; 1cD 25-0@!HӁ02OP R/-b[YY5*4$8gJJ$ZVR2E~ ck~Gh%R@L5+|/Y]aG#?Y.WnJdBs~$=3+͍R0rжm[An-qEX'7QgfowB0($Z#mcQE)%&] (j܁m˺Czé7ZX\bfm7L-6t]S(D< ƨoF/w[] mJD iȟ)fF UR ]2䒠@7%Ɉm]1PX1P@te]1 o"Y+6h-6} 42FF0*on;y [D`-2[(aO[42ߗaNrTY-!o@[UuP5H>R38?e9vJZT~0|.Z496 iR-.>`p~ 1|2??o^bDŽ9N1Z9^Fv/꜔,c_.W7WkWk)%79~ .gGOx'\ڝbV8kOoؓ{{ ӦA+6=IV ց5G+L;%$Cv7k%?Cn#ěͦ2Slq-Z>Ě/|؎Nl_T@x>U_S=R}RF%oHW+\r=2ݤKFKc#D q+,t˺r7]궭1l_%8#)RѤ8pB)8]p +uo9BH[FCKQ]=6sчZyiK)㬄":fG2:U* @R)wRʐ!԰.eHLU Ȅ`F6+Z_z >st])HQ en2>K"뎜S">EiK9WNF35~/]pygBX,Yg듧,/N]fv/Ѵ-~}%[9~?[^yvG?Mf,O5[/ë[o͙jQ' Yw^ ӽfBZ8~)(VC{/H9s`JJ=ߺ{_{3#%|A42 ⴱy-;;,;C]J^}ִPR`mZͬ XbLѭ HUM|F6 )'$fVqhҨW%w} WlA:YQ:q,*dERl-lvqۆDX}@F$JѬzs6fY+)S}5Pk2j tU (`)&lHYNJ AK)#Qic)GR f1XL;yyVtX ^O1%Sho%18PJmVCa,\R?WZJG'Sͫ‡ QJ󲬵.: ޾>GyKF]8kuőjg3%(ߌ."R5QjU?T H;E F5LmMPrq-*J MaƨB4vwv =xFʅgkA+q;b<~o}z3= (˛oEPSmi˴WGɣ0̈́;g9}b_/WˀW?>_RWJ1םQ{^cX/Y>'>삇/V=xcn\gS+wv.GpFRނOpeoBn\Öd""ݒՊi1VڦIѢE520RF9Rr1:eb"&3L[F|@ɒfIK)2#M`mf6 #`V̖ˮb!+EkFy-gy9-8hd2q`Ɖ/1xlcE0-!09m qK+ΔRU6lVUmP^1%2@. UĬh+r Ŵ㖈]Y۵oF׎\ܛ)0z|H( CE3'quĔVMouS-^) 51QUN/Sm5̧"}ł)[rkTaiL+ N*6n\YlRs&E)F5QZid f]ͱ_QMi,!f`LfRc))EDc&_F?Zˋ~13=_nISHܾu+W/֢L&<|O~ʭ!Yx5׾gp8'owJ9/T/:$^oNr) g"#[Sbq~ Vq|rΛ79X{V>H|i ;g_}ǟELbY (bܽIN!+ %?a YX#6n)Z:Sp(/">DFS\ҢIb᝖\i}yхL⧭8w_T)ۼl?Qtewpyt,k7derq\7^{?[3WWR 1ïxUzUh8kգESko]}v-O,уS.֞w9i4Ic9{Swq+pi&ac޺y Ѱ`ur.7S>ʼnrT.NbqM~/NK !gƱ3I6$!㱵mv]\2MckwI ek#3Lv *Em],NKlu,9ki#ML1k)KfG¸eO^ "'Ҹ5Ql3d\9d10P|gSJ|>G)ŷ{I+qo9}yG$9E:Y_1Njﮇ@L=RPej1Mr561kOd_qٛ58:Z*1ۿ#ˇ3= zt1狧Mb5(eXy~4nS~_1=c<~@w-Cڻ?_SבoreߤOS; ?G7Gz>Œ> #˓/ ӳSK%_==g:N뷮qp1w у{-Y=~9Ӯ!BX^<}?I51/onjZqz_5{+*XѦv=V^PJ!P_6{lXMe֨-BwEhoud o6cj9 ){o |өJsm?Hk)Ӄ͚csP(]ofڱn:S1Z,LftJbĽ҆4b4 #ܹ~[73m mcXq/wt0c:;7J_=f峽v'_|u*67]#J]g_p)t1m*AXxD<Ǹ?ӓ-p>9fe}0HȎ,@c7Cт1enNy%6ՙی68T4av@IadHbŽ+~?0Ζ=ڎb@@*32^޵X#V4Sծ@=!(?fItJ5J+}u$nsӗCآc7)p-fR*[i3†:[pP x*0qQVtmFV% PG !EY[A;$< 3G`?^r$]~a*LSsΌ?f㮱֘j۸o7P!)t ~ WkTf]EE 멜`1Z8yTk!kj=)gK~~OŊ}='l;AkE#05V:gZt Z)ԥ h]xc)HiF>NNNk-jEG9W&LKk >dBH .yf2uzL\qm|c8bzOo^-SW׫?:ߗ v/va㏬ge IF;k7hS錊Eَ?L<0X{pt}NW#kxfwO9ý+}` Y'L[.џ=e<-\qdcXկXX}d^&3.vd4jL^*zDYkA QjLe jeZmW "U$ln"[+p!Rxl6M붓MlFAD 17x8o`BTwѥNtM|6Q#kDP)aXgkbNP;\ZA,"UXfaQZC5 HlmvcD/1J.hU]Y_+";mTO * E+}}l=0Ɍޢx6T'/>^@䳳/nVu8PǀN(^^jE^Nqĩ޿u8ᵩh8=cc~w~>c=5r;>?>珴'o_/W/?}:8^>%sg|LFV}b9`K,Nú ;چ{O<^.yb坎6fztoќ?{W/ˬ3s|*7RXR:.^/g >u*̱J_!`4@a6yMWi<fc4Ęl6mVe#q+܊)oR$hdR$/߀3-B~M _. Cƭ3ytMlirvf^T' `Se3Lwc1QrdoonA|*XHh%*xAבu] XGL;uS~MG Lwب U͵0RWniӜL @MP&E$ ymޥ)Qѿ1/?hBK5P5)R)%Zoq֘Xlvbn*9"ŕ.q8s$e|d98abM :mp% y͛|ezc.|ݷлճgxF8W8p No8dލ| g5ʿ/_nj3~aؙ@(~#~v_vcyu<*1J)Ws)1ȺP<:^MH5\tM {Wn*SnNxt"~'Rٝ:9oˇsx_=9aNzyms}epqKbLt)7nf<xp6ጥhrLtNF>%q Pz;ݜ nXKeTQ[5Bzui*wsַj1tmSGR%gG9>>RAT}FSjlަmF*˦O\Q+C?*04$qY$ !5A7X5kxLY}4d*v9[dR€jZ>_lPMel q$DY]C[7ᣱ4-(/<{~D?碾TGțuR B:d * Hob#J"kυ]/W/W@z_:a}cs c`m'T^5ir|h!8fLȵ6]9qXcK4))ڐ}O@-W5h-Vg:6eZlѤﹸXK2ݹ3+LyiƑ a+͋+\XV󵸲b!ƼEcoq΀tk"zc9 7\5&pADT8v*p4.E.*]uU[ 䈊F&* 4C_pk=4w;w_ ÀіUV0m ɔƊ|xJX,.򳏸u8Nvӧ¼_?xBcL'Lxo8?WgīRm̿_=[0_,eZ18DӝCPZg\#52ٹrqq݄l ב!xBܿ~gΈ2<{xǏbQжN1؄VhcQ +c%)Xz.K9>9cZ7]v/R|Fb2F8WgS3v߻SMT < *Xi9ˡYP-G}p6wYbafB(#Sv;yh,)Ay ,RmPʙuxxqd\t"n=L}qRbq0H} Tր )z Ner 7ʥhq6jQtتyƖ3|cNPR"a9XZZC۴5e}"_HuLbhtKSBz#j|6FSP :gSsc %btKk4URPxj,q1D*Pl*ay\u۷!,lgzO9>>`"^57ewW?Y R\/~o6jA9~)ݸ̯>1̧0>{||B4̷oܽo/KcW׫2FWPjVۮ1G8mٻc5N:J;A5Su/|(| ӹq'|Ž;wٝ4;Ǽ2υGz2n{<geJFP9xbԵR&C ]YbU|݇]^b=/u)ʍa~FQQ*(Ɓ kpsf l&TBo_Zƾ%ey:؉cB ZDm@,Pq լ^Y)b"`ب6r)"St]GӶL*Џ^Ĩ)fB*8-ⱐqh@U2Ektyߌ7N ,H1Sd~FV%g\XF fu,~:Le._>vFvSl`GvԞ8 r@bZRl6G"e٩ꂑ7r;ў$+0ZX{adXhۦZS Dg2ʹ%H4r]VDk-;mJceÇȝ۷y|wC~t* @1?!F|?f|^ER)xV)L1UsYYK)p{=`O/]xETk{v ʬQpZK?F\vA+9G'zZ[oE~ L&p{Mv:Ƿobə]R珙u3JDvX레-սk.F$Öۿ>D}Ϻ wyun޸F6R Ɩe-]YDwm2&5o,~(sF]۰33]p4֋ upOc'+H 5 0GǑ8PJ#6 Gğ;Z0Et)Za:B YYB]jQHM#pDd:'95-o3vVh'+ f">mȮR#dCLd9F&[_3KAHaƠ)>%Ci|70^r-WkP h b=NAci-_b`>wg !D1R}P\b-֌F;tΉ3/,~d= }O \|B-X:Ł# .5>)&̓s7x ' o}5Fa=Vre~η!ZRo˘ Ei3ggkOٛ4<|1!ng1KDaͬJB\L[˵)!Csy9uOWWJ-h, P8}yңb3!nGQ c[V<{mMW8Xq7n୷ޒrpuɁRie̿;_d(K\̇} :זDJIQ *\-rŞsFYǘ1~GOYᚖ%4D5>FO_W2YEL >/3kTLYªȌH4qdudv *h ɏɯa\'d49J1`!(^HrfzdkY,pL;d6GiЯyv:p|8^|px4-Q7dmN{>+.6˕D2gɡ<9SA(Bh&4$(,\cRʼy.x?57f=壟>;!4}?+y&p(ΑJaƊoz1Aٖ[vϏe33MNW,=ܺ:zX3czu*~]*/W^b2#BN ?#滻qE%;|,..0JZ%-m[oquV%mhc~+6FӏaP5˥KWwϸ8?qX`Pڂւm*rcj̭yVgYf)L52#vwf yhkI8J3H"a|~[nɎnѸv3HEdlY)bڹ sB[نYadwª)n֊˗:l %+ni[Kg#_rsu*T^􈵖v ~)h9p0,}f^bGi ʶq$eҦ%h#[-]we#N T$i1#8 =1t+h޲t-(ʁ4-k!D2J$8qX8nQWGQT&y-E`;#R0 `!ƠtDD _{Yqn'ݔ8k޺Jw:c{n;ljs5X)UZC G/y(ihՒmqϏNۜ}Z!W*EY /b26H7n LZ-Ŕ8:z;ɳgI6E2CȌ11k )Nvܺ:_|g5!R۷8] veer5 s{Uj<=]qb`gSJaL u]Rrپt=_r|f9v 57T$wfwg8{9) (?#r)znɥ0mr? kb)ܻw/jnd4yu]!?d P*2g4:cu]*v߆RWhY"A:(w0iy44ZbJxqz2 GbZ"<&F 1 G5iKК'hcYt 4rPu-651t7'KJ X!ЙpgbFASpnS3k!D tRthhSbS>iKі:,5Cz@N1H6ەTXzA{43_Kr7G#!rAt/!=g4yU9׸7Yxe>+,ur4F3ެaZxmtx3WtVsuʼs4Vn_>=g9;31D(9a9wocJ`|c,~_3ip X]'0b^r5zxe9rscDb!}wWm9oN۶ O۸m2PCs3`C}yJwxN1E\`lC1 %4`B e\Pt':Af%{z>;צ\:ø2p\-z) .0ca=Dάb`6Q'[-Ts¶nQbDb`$_{JHI+!cSvK)+˄"B!3+"^#gid*mQ%|O;1MΞ[0/$JQ2.e|omQ)ȟj]T9+@7(d\ (Scru≫ǡ|^E39vJuovUKVLe}#W{.֓K4P"hMC*ł]_㬯^B7iűgv D_BBD1b^H9k| )aVV09 Ww89z_k|p<9sZxzKn bbVܽu߽ėGx90.ϰnt5|罷 E1/騃8_5'Ob_{U*~=%fv#e\JRsK),{{[w^\~㭫tEkg-x^; 7w((v~]?=ˡ0{~Я*mRŰ:;?Mf9W(8:_X =_c+1RrtNW͊d-Ek>n5p1ۻDw{W~mkR,zL\n\oGI9)\ _?7I7/> e?X~EF!f4m#>J+iդ0@XQ|gY(bn8>yO~L/ϣ臑bAJt.?so΃G8_$l Sn53dO4ʓ&sg!'qMQ#Gї~ ).g{ )~cumkWKBHF3O³=Mf2tfeLnځiyJ eeW\ ۮ!H'RhS*'sVFeH=Mrl1J[J)"()%gb kJš}gA"sZKᓢsXؕt G M[ a\?9wx7>sq2kH12.QJ8yzylg=^0 L;)[( K^.%}m~?j.G1֔nSJ]]YkUY8_`ҶΑ}}E)3[^ťJ&q O=8pxI#~!dQ\Ŋf:'γ_`Z^'@]r IJQ!Ъ#Vk Sz9V2DjkR\L+7+9nhܸuC~'zM 9 _=$3c:ρ(Rn15$[ʶb;ts@EAS 4Mq`ҰXvv]cq3R~dY #ӮIZc서 }QOGnP_@s KVŢY†Nq}/JݟS*&s9Jchqt.~ENB;#@!g: R5dTNP,-]1P"f@U8m#(EI׭ Jm0-bgpM~r6h[32ZJJĜT2QL)!_[cȨRjaoِj䓏>HWۏZ.L8Y,E?˞g`ᣟ]/~#t</>#npN1JQ J nFrf1$FBVAqq3N3f6٦ҭ9SɚKjNX*td\mxʖC\HXhUhĮ5 <{OOI+LMMF#)2(5%W<0\RH҈ r)2fQ՗i*>>J(HOt ۴k@d߃r / 0aJDoA!}BD?Sy~=ɘPGHQIF0*Muu 6P֡ ؊5[:T nWԊnIqK\k^]gt3a>IpQNqISV5Ϛzd<gAskJg%@T)qV Ĕdȕ6pT(RNY,9;r-pKN>;e|%wht=^|pٱS8?J ^ h }R)~-ιF 2<(Cהf"21Wo57"D謥d%c)*s)$_$@ +h TDH^%Ú _ԂU2֛gb`bꗫlȌ٢ #vG Hؚ*XjA z3bv2K٧ck(!'p7y7x т Niא|`Xt6{;<|~ /z>Z]zvãB35La\C3'￿ 1"b=AJ|7; _$ _[pSس>;F<;[s08Y h-`8͛7Gs >N5 wr-O0vLJ7tt3n޺ýx3tuRӶ ]'88Ȏ."bI@Zb !)Ա2t$l<8m%ۙtZnȱOQvB*Q|@,X`Gu׎2(mZ.[$_>~//@&ud=!%a)]rvFXUkb&Fk WLc J9ŊNS)Jt wih;92cBQv hTdmIG+nU0ZK 1KuSHG3φZH,xJ (ېVbCYPځr@URD"aA@"2\VȒG> P2lCLЮ@؊.%QBFXJXՈHX dvm"EXےIh|O =ΔEq o"2rId}IE,4C@ L#g$DڒbTv'QL٭)Z-:)i%&ϗ_|^ '͚؈cN&3n\p ^CӴs!q}orz_aݯzU:^)UxN.{RYt 2Qy$X-_ӓ3coڰ#9tec9؛< lO}NK/{p#:isɢ,֖9knSmcѪ8L $:GL4c`i]Pyҁ) ""RMuD*u3ы\6RJKt`[TicaٯQ1EvZy'KޘdnM.׫Ѧu@59eXWX2vRI, ur$9]X}љ|FQ?(A^GđRIX`S@wSJN4F]5Fe+EYEHiNEJ" $e&Q͹m :y*8{c6FH6Pŧe%`U49 "t iZ9$0֨vZӴlɣhrTe-R#1FuXU#越/ EI,4J%kiKPCkC@3˭k'Mc ȭ9w}!1e~E^s\r_} ˳ƠHLZ J!U"JA`'5 >e6,XȞ ¥OdTQ4*m3гRW;{T;hȱDU^w5 V[Kd(r 䘈%rIQ`GOʚg.Pюy|8 5kUqJ)J$#~}d!פ6'+(a:u5MkV'C"f~Vݔ0-ӢLCQ !,h : 8HTw SoXk(X$*):ޣ&Ler"@Xڤ(6LUmZd]F(m+6k'#͔|#GJ6 :GY ZJ H%_W =2Sjy1|,LEd*+h_ [gIZ JI&/r 1js4JA1 `r.F7DBzM5EY?),y#E})!e!1U3 9F}0i *+8p!cSIǏ:lɘՊXtӒ.m[a*yS+)1Q;m)b 1rcJ('I JbSuM(E"_Xt[3A[9 =ʺ:Ehd|#E8d5)` ) b'lg21-T=A"(dWeѣl9xy~ZQwRP+YKI0QJb>-h]@[ySRB7Sl7e3H) *L:DjXfS.7.qimRA֒Y XB8r8dR8i%)9|k Ϗ^ʯT|-Sqpmƽ'Ï9}olѥwGo^"dC,5j1s|zƝw2GgABJ=NxDX2yzih&{12񃏡?rQHLrJ7yL2 ^>XtYEkmݙG% GuCD&GQ|)2Jяth-qDk1Z(tCz !GkƵLRTHu):rNdmkEAKAgV-!zMk ckL慮NqD?8ZW+(fTc@MlPBs֠!Աi:r gPB9%kj1ƐCkQaA#:B ~gӆ Rh)ZK@(4DYX'Z iے\m^z@i |BAklWنה H#9&k1JIm[tA1Xw6ZNruK.sB;u"q( e->E6ܼKc-}Hd]jν1ik]#>qi!IFq %m1fϺQKJFylEqQVJ"=E_!FE?dV~52F-Kk4wl]s}O:08錔@kr8FgRXUW:fǁa*">|B2P4HKjRdP%<)cѪ4x-2M;~N$%t12vh+;uU16R :T)/y8~2TpP&'irRhh2T O '_&-81Yk=5#biT#GOWhӒJB!6:i87ƀϹfv bF %r& W d#ʵ۸= Bvɔ,sk2FY\@f>i:"y1smoŚ~4FRU0bڶ:jFF/šsȘ !ifZ 'O?'?ov> zدZq8k8Vͦu8GG< ܾq16.['pVCU9K)s-~b%=8˧g|/stѳ\^6x/~ɂGhmX|i#-F3:TEf7o6-}ˋs]Q5 ]`XRJ59a8T!IVa|*{\8ˤm ECdȚ'O"Ygq,A5E3ۡmmKm 'I]GIFHF"&R[R=Hq=/,.z Z4r5c,".#(Bь)SrdXIm&u6cE7`æ@ה0RLESRz$@4 jE?r $bU*PVE[F?,K8QX }afKX 1 uN QzۡmCL4n> _0N`(8mi%+8HVMh# ۠9,*~4*G,`3cϨn&"Ԣi0bDЂ2%/6J)a-!-[\%SEsa[ئŴ$ .Y)y,- B!f|1Re9К\*vww3Ǽ鄒 CgN8iPPLgZ#VaTC )8G\~sxQ+ie2 .͹7罛xsY="{k yn'_>hle*vmN8[Siϕ 1eF|G~Kfkvʓ٤՚yV7FH wCO>& Zlw8E#VJkW]'{*ڱ(&jÐ3h<$[5OY.C'M3 LcIZy\5)OF%d֎8( A{hgBSJn5(B]5)$5%)n2 !2a\`6-ttx Y$>NqZaf3r~p%]zB"|$nI9Ɣ0{)"ltZ ͭic ndƬPAG"% YD!ikZzjR 2XY'q"Ȫ!t ;NqDCRjE}K@I Ǡv(;I (8BQa E3)0 `DkhB)qZT1FC C1"TڒBN9GY5% UYu:C,@P`HQMLf`h%\ک+XWk2(9ɺ*xc&(/5Djm5a FzLSnZ% Hԫ1لeϘ5#Y)l7!/.F)FTqn Qb"+ͣ%>F]"!bIn߹C?x~"99FZgqFx}6Ra SbRYOX2>$|LQ{ӆ01* tcHߓ ޔ)q@}P8H7^)fn&"CKcZ!Ku*aL,0& Ab9=!;\ ;꥛ e9]#*+r`p dRѤd \H6rXg4Ʉݸ]ЪPlW=\UEi~k4UC 4+v`"x9 &sMa[)5AiW Ov0J@i:aA(%OܪɣʠTm#"0 Md5cB['ß%ofL$Y/m]醖)JAރj(!rUJӪ_Ѷ4BSsqd9 :;Jb7L35X?9#m'mqvL\99MZS Fn(lk gLE#iXpc VkoQ;+1{]YD!}Z1J5:!24LjtF]ŐٟL;YCo0o5,N3;'^M~.:rж {TX&ou-ٟW_stIR;˴_{&ݔob'|RU`ֹzI4]+7(^ +}Mn߄d82"5DG۶N1/ABb.xE:saR1LvIyG0Vn$УZ4ĔHYVnsYf!23gk^ۻP ICʚE`0 GՉ8&+SѪJ*O_a\$-.6,WKJ>dN(&uYhQ18UVDB]|Q4JFfKWv7>J=!oj)`&aMGO1Nɩ5s%uRK"ZcL:j *faGN'Z1jۈ(cGgh bDp rV^B'$M*6s;ÈF}~??zŤL?z#g)`uK&{9?!pojMю\#2r./^۾OW_+L'hspߺuk׮q䢈~$½%1i|wMg뜞'XXW{A|6ֺɴ5UqEL̈́OYk1Yr),sÅ#炩}t|uW; +йvhгZ,1JZRbG)6.uC*%)!kףTΕP1/`3q8~Upٴ$ 96vס9Xq4m+,IB5]nkI1s~$i&s#1z I0H#a(rF|Uwێ1Fb*`tl]7l;:rǩ\Sd"#F A91@oi:h$N_2 (hj^Q2M'1WXǥ &=9Rh([*yȁ]u@Y N]U6㏣H`kZ~HJ́X*iJ+϶(cY-mK?ǚu l*YUWKa2c6q`neg0o`6qX# fe^s1xS"1  jPdFQruLhrk F6F;v'qFZC#_oOv %g)hfs "QœmDZo3ơtM]E3iar8~ʭfg3nLц39Ckv:uO8zqͫUy)YTqr@)(V#V{%e>`%?/BQ̤v=.]ʧ_gr. ֵ$4!k, PUE8'QƴY)tP<_Su6\ 8[/a9KIcY%*h0u՚Q FbN4nJ["[L#& /[SXJ=g.^noYmι־[###2]]ݮK2xA qH"@!#d! q0Ц].\YyȈ/k9a|dUucҹʌ=<ڸ-9"G8nn^ APj6tWd[둭TZW3'}#Ny @؝ 9Ϝ .{WV38邋HW)yledIX3[4REb7Ħf8R4~;S&j BK"G;NfiBjW4g۵f*6W]*jA}O"xuSM3u{%LeTѝqZ'lF=R[Ƨ~}ݎ3cmXƤ9[l5[ޜt WO e^FJ--ol$UR8.ZL ۛ;th ו;m/=3EGvhjqoI&/Mc".Q,SMx‹'ik'fED+lAy㰮|x鏸fJu`c:w{_K8/?_9e`ϑV؊[;G Ч6 =8g39潅4W?$Fy|.owđ֏(eeBK)$ڶesXK.t(o%vz\sqKOkUҽ{~Df_߰eReqQѢcgzH65+m=ػqX [:6H }Q:/r&GխyV\b$Pm6/=N-{7|alrtZ8>}m'8mPi ̓S[;ʶ43g7YNͱ֌C;ߺr{s$$ݶ sۡ]/X ͗ /rvN F4 4螈xuF6Z`]ݎ dV?筀1j1q# (q# ={ ф mpiPZ>"} |BUZ@VK@Բ"lH@Q4{넗BqGqsP<!-dEuç7UD`"%c.tOZ۰\>lMpN||Ɵ{h>dIˌ,R*Y?ak01W~k_ƯW?^uڀZW6WϞwŎPUW^سѻ{ ȧ y=pȍ{ě g|廿V9'xKʃGxwyɏ͂A;hm&_kAz)0ѳW t%ghJJ^?:_9mhٺw.w86a6viөBXiĽz )xp>kiAbA2vč:'bXF bsOF>d{p{ih]BB kݘjŐS; 8+@(f\ D-† Nmޑ"Pn6VҲ#yOH3t -{'q8Ur`-W%Et GXӇ@ zbgwq4t`?f4;ZA%X`/D$[-n4CY Ji}: Rl<ݼCK֬-/S0Vdd^1 yP@Qt(nlx RpfA]WAR8 jd'J*twno'윥v^<AtNxsَU9Yz4(Oxqa{Nr_~#Yw|JZ)ck(9 ^[q\L8~N5o}-o_y9z!:׷') G0\{^ejhT@7D SjZ'l3n㪶 ڢS[k̅=SF[| a̵C@iH^ad/c28泅#t%x5jiט'%F7@7֙H$o=D3ۖGeZty6K6q.XqEX;Xdt+ *dvHIH?G1{h(+6 YLkhx.DzZAP\4BG,5tIB{oġx3ƹ-+>ӷ c m;ءCŏNFi2:$jҡ8X!ˉ6LNh&rqi#& )zJ1Nk?fʡzN st qp1{N,<ώ'\z'>m""SC4c΄PgŞwVZSvӄRYL𞆭ct;R*W=C⒴x?I7dٝ_t` Bœv`A P Zm)xϹagl^?yC RƳ)"4Rvx~(4S#;h2 d^3-%7"ڨB>!K74qT. Ҧ0iPfZ(Fv7M̜*^ SyNvU}Ft5.h^I&ЪaHigZ8`]F/ j3(ySs !cgaļt'D*D{-~'&ˊ,ߊm 8e#c OU ųzSSHБ=lh91 H8SN78o^}?Q#V!t V`mX;M$u B /r!Td7'R <;Z΋U o2"[/4MG0Ejn v3a(.0Ajf8z# u>1m-wkk#.rXXMКq+k§ekj1@~Ƣoyyy7j'tP c|ӈ}Bof+NF=KmV?1C|{'G_|ﲝ|.XA]3N[^s} :?5Vb1 ۍw2f*x>|?|/HunoL~Sr1L6%HV'@ B H1]JHO/qӆnk(QjyTp))Yxl||l5)r*2Bddf +0 9UlVOOuv`ֲǒUv\xc[m&SzqvXebD@Nrf§ ,^$C~Tykw|:Za !b& M }-PPnnk5x g A( OYQq>l@§۰&v͈*#pjl~Lr?*/O|hEOH! utsjL.Cy' swESlِ]U); Z Y;)o3݌!&=⫃z1vJv41ï͙թ4!aa>!Q#-r˜oc.=WHBucg[响pKnt5QyLH3^51a.@Vxn4Fzkr(Bv- Tk-ہٕatj3V a6QU9R2<^u8&7kN S3]:E AqI]!-t۲wPGqOt"]EjlZWZ^Y a'S dt6{^3~nWGnO5sq"8Gݎq+m:7;=;CJ?D)]|5}1>H!fK.xd" *q3>\ًۿ?+|+_6/#c~ϼ}go|rM.4iS{F?V*ЕZ?ݟU맏0_7-'FlCDAjlx-Qeg썳W{v[[7s\ÉiޠDH)2ϑ)Ev'✁n'DLF4VrUlu *jbkΉeiuƦ` w.xkx*"+W\а7=i%}}<̽C1ea4'{kQNG{ĘXɁ&%)΂vZmNEWj`9~Z+dP\^eT kĖ3jp(m !C;EKLvp8#K6NC+ZGEJ ˙u]] 5K7j@n E3&Gچ;#FRt޲q; ocDq^RjS=I²2,,8%fbcn6cn,sϚ F\R.rFzvSZlMЮn-}Ngn2'eƩ}YP26 B 6VY O3t>{W'/} Lfta3~£s[(r+~B)9_&sŒxt~G|?L̗q#)xw/mbonD>89ֳdD;KFk%BPųn&ݮps8ps8p8iJLBL3'v=,ݞnncٛN_Ryx#ۍcS(ٗ#>\Z:p 0w ]C\g$B@ܖmIVFo`㍮q9oArZx87V8n\;G<{qWҩ*#f_n~[*Sx{M>Zfb|Vhnor9Zg bvbƽfֆ|eQ^?vm .Di{6B*&jux:K.fTwZ]PVܴw4D:0AlEٜGTltب8CRT-m>~UA^赙S) gee.SĒ}$f]Y\ ᵘu]~}:mUB(>p;~o UY~ZLAѳ${Yʖ7f+Xx]p8|+TÉ^3D,[q{*8'\,FL1$w#̧u{CͰZ}eJ|᝷ou;K~=~g|itnRH3)ryܛ 􊔕Xt5Qjbk`)WW7$A)B 8X8 ~E0DRl!7UZdD7&{.l6gri}>k+J2Гg'?޽KSˆjDmR3yeJSbeo4htc'lg)DZC+ܪZU0R4dACU{-X/з1d0a=Z%(֬2Z= bم!"8ӷC{mQ'PN~_F3F-Sz?hFMgy1}4(|N %*"իB!QetD+TT"3WGS+noݙ\Unn-q;)XH/-hq'P VfliV%uP!=JJ /׊Z˙5`lcDdޭkv@sDu[A*t&3[gt}Ł. El 8nt} *~fEno@ϯl|,k􀮖OqZ βYƓP6=!"mCb$H=jrжZ!o}E^{Lqv~AY<3^ )0da@S_Hyv=s8aƷI-d:!&D`m+@0, Tzo4g#<>ʹqD6Kl'>;{⦚t׎]} ?`=K7J/G?8)$rnURZ }@v-. <|M/?&9C_ TIF`<=7bI& t t" j@g3o & dLobB:r]U< Z)p+M;^GG I7e1V k(}#qbaPţj> N&nv+[vˠ _nI Ϋ}lŶ@Yn3d\4Rk68g iðXF/:yCkE{&hbaP١\.KfkVgW9'Z)7@s{2ZJ\':T[?nMXKOƥD^wkB/˺̓Q?7ԯ/G?[8cqK|lOW%L)xu|A?g~O|Ou] FwOp`PiwdJl x<|:=o|!^%?ŋ}փd[o\y-LSd;~LJw[[Fqcydoͳ?q8bPl=O=*fa~FwxsQ;{?}@Ƶ\*g"E+|Z)q6G|*>HhۉTM;98㣠j^d"5P&&-9C12f[zffo>$;wDgKlSJ8!-O';~Z走myng@'2 vc_fױocvnIc\2ӭ+ǦzV[M{ncTv* [S|IO(dj_TFSVݠ9,ⴢG J$ݠ=" ݮYi7X;z^Ǭ߾/Z6z+hiֺhzj Հusdu+Yf eL=4c IYsh?O hAǬBk7-\`HNnb_^|;ק9W|O}r>VXs!X&C|[KwoOͷ`b+lǕ-o_\0)C|;?_Bǟ~j[N:io^츿#J=wO9g Bp[nl-+ٍq zÇ?8wEL=+D_̲[Ln!mQ;7q癲ePZ)U"/?~\vx+9OW,Q"St,'ze-`-Qʆ᚟&hu(- IbN`ނt8 k +w+7'D  _ň$$>W[nl6}{oEI@NL%iad:Cp2 [q 3bBъg2 ñ*)5lmHVbzm(YdpV6StY nvW㸾9)ݾɅqs8ւ),S$Fi X%>ⴭX=ęx7?O?*ڄZhj|r;`Z ?y#none[^ uԹ>A+Ώ>+g+'UD>EC"A!r?8r3Sw~87͋Ɛ"9`ˇ'G'gD Br+3~*\?|g!W5[<%`Ko]_|炯}|=_/q}.{OW'sx[O^։v-RӁrsj\x?q:\σ_!E Bn '\6 . nߵCRV>F,0FpЀ9!%i276'@LQ[3W|Hh`ڌ_m~Qk HvY8Eo092V4Hp],ѷ#ak2%bys0B}Fz@bpCBd%txn={aիC[hV8GNc|0CqGwLgfmwA;T+ZZE[/gx<E7ͶIvӛ;i++nÎ).15ے 3:g0鯕G$"ubS|Jx6htF:w;f?{bc8V ݎ?{ =osGO^~gOh޶IOW_eY?!N:+ޥ %8wb}-ɍķ!g|%wÛ45/EUnrmk<5|n||(BrN vZgLs |ksgm#Ol]k%F;*AJЂGJ> ц~Yw zFE^48Z lUwLSԘH{oZ a9char *lVA'> =@!MB0)VqqԎW[1z^\m<䈧3ͳzKDo`֪%U-潹 jFŰuEF7b4Պ1 :Pj71{:ӂq4#|§ m B[ h~A+6m7* " 2lCe$^=Hϛ}@eE}Pqn%[ђ@-C [W6 X*_t0 "?G׫)FGW46jɜ̲ۓO8TBώCKv><#Αr}sbcê& [? St~҆HGHG<wGG~t'"{BG" oPl6wjnǏۿ#/ ~ ҵsZ3K; '&])>xMo|SN78lfzʼnʕV`.p>=7E9Jh.X)pq\;'87%ơ meb՟O}iiz ˉK—ެ&8!Fn` <<9]K-tBp2.ISZ8? <eɾ\Xf1*]xL)a(xtRijHM+n@uiN DhwH+Eh'Lfbf3Qh>\")|E%>Y{+U3svӺި;XEՔ*8ژ#Kf?zw!J[U;N $وAԂ/'VC;gzv Z2" ^ ב0>m8HެK1.uu|jIajѡ ".nE D\7eRS(+DE:{FPXZl@SgEe̓/j:"8 "u sw806Gu:4_og _)*tKnCJe4k|i8X;nzD?fw3[w=p{Ző^/C9f1ԵʧOoo~Oȇ~8Bw>'k>וEj7S2jFsBq,r6{$й-'ןt{ IVp(i=~\Uǧ/~_ eo b Y},mS{a7y^||UJA7;ϔqN+Ox^aR#Vj&8i.ڟK hm䮄G;S֭`}HR=}牾=݉|0m5ZVSLUH+YR4۪Ɗ%g29Bs!zOkK7ُx~oZ!PF\+`N pcg8Qdxz04Ftj& V64 4 xX!`?P7>Ǟjy@`ښADOL{: PJ]m f \2]^ Z#(}hq|_6++.L38P]i&91-;mޞO>w*vw8\/K~:4ŝ^:4^2ǵޣ3;9K=>GSS+~k)SKc]{09j2pKpV>YZ[; ֭ӤҸ -; Ҵjk!xHeJۣsEpH\pؔϞfs!- jELLnj]8'8OV,Y\ E%}L M9;[A]B\-8ۚ*Yo[ fӱa`fND]/ݘAaxJ@kk{+pBwޒa iA3 c0`OFoę"a]N+q+'[_twm][ *Mኝ"eZAt͐=l0-xs88O̾tZh*gLi"hyrBTݔ8auϗq+88{5SZmnNn"#쌪dB'\L\h,j$sSLֵՋuz?^u,5^\]__[ArWd>gggҸ[(0Ov@Er 5!7O/nH'^؎Zr}Ql?rʋ0ZcءZrVR Ldݎ\,.f⻹6pDk{ˤ wh[/ A6iaW >X-^AD&0Q!.v ȼ =Юf8UdO"9gkc-R͚bV˸H Ź3( Ac$z+<>1!Nj8{KlkzZ74lzq/j[Kc(~B4td:jVV(b.#ECCf.Nl=PK6O5hF$l]iƳ6Ѳ<@@{Nwfۊ${-jɖ nVZtfdRVve.> OvzYq$xm6HĴ[luKP[5J3E! Ҩ5Sպ6F&\99pC&xamy\H1ΦDm׿_: {9:ER: J61Zk*FNaC711uD=_.xWd>}ŋkR7|[m"ˣڰn51Yj%Ĉo;hktqBqBClcV@~) N77gG}gu^2[n<7́`H`'m%@)滗&_q3g|W^hɭ8]$L6"WRJȼ爫~;]`%Nv qm]7HC4Mθ>Rj#a[,=yN[p{uZ J3|Y6;'&CFջF{uq2;K}mqth"*놋v4u n3La"q{t~}o@\2^ZM^nxDEG(}|:8[B]p˥}޸:-t : DY{ËYZ>%*`܉C^Wzxo.4nL Qұcf-z{gzAZ;S9rcqWӗ2 URxz'xc#=!r(˲'³,Dǁvx:FϘd'N=?ۿlOo].imvJLZ}Jk3 uBKf܌IΔԦ(U_'|Jm^k1zyusj/1 vHਹ"N׭ +"9wzy'_]CfEqvƣ4{U!r*,1x&AvY-61e{ᐡƵ~U[Vp!~o8͏w[qJo=cdlm{H|q1P ,b>TFBlL5$n!U;q.ΠyuCeje{r>$1еPv|`kpsuMq^XseBr+yD}}ΦJi̩\Ff4Ӹ!Bnzof ct}Y(2;+JmbN6Z/Qޭ榬vkzߝ$wOcYW[ϷGN*>w>賮i8>OŵoÇcǡtje)0/_p;_2[z-Jc]L/M+Y 緅&vnp"fˊNmC1y/J!RgԧgV7.xmWAq!q2͊38莑J_!!Pz1 `+!+]6?FѰ-~waF 'ZUD ̯0jAF4YR*ҕ4 F-)ZFZ4tu˴&+tf Nib e8vlhNUhFmu6}^7c;|.tnsnd-2۸%[4(eeOﶆemuh{΁ qunN39}61n"=f){yٖ-ANf;}e#nn iuƊ"VF5ݶ^(CjA7P (k%R;klMܻ^K J H/VH:7lA_qX!l@p֘"Z8nE4PZ{~;OT=&ʱ.s˄]n?qrLJ~w~sbw>=L+\^|rTxQڝO7?__}l~=m .E{NV{=K4g#z1wj&Ǧw/O2'eXUeQ#]ngn0u$Mz.!.h 7ۮ* &JHtpj3՚y7/[Y nf\la5JJFä=+^vŻ ^ ϟo+kܼ8!\LylQ:[uzCķ\8uRF\PhFHuqp :agA@CSAy xSvC@${jaqTm6:ApaB[5QmmRx".vy<>%kj0*:-f(fU '^m tu"5ۯuvLy.2Bk.6 ڪ>vø_w1 hV@ 6{\~~Ms;>{~wl~Bn`ٞrਵbd"%\E#+y㭦/_w]pxsoz';q|/8jֿGu۸Ɂ)FQ/qU 4j3wE4C9A:3zbΖyiB,./ЇCĝ 8 "9\SM{OG\!D+!.sG2OsuM1-f@ku\sVyڡi'F|Oa{~n/9: rhk'3t^ܮ '/dhg{&i\ݞ*\MlƊrqqΦ=hX{)o,n@R3؟ED#$e{Xf EQ\91 [8a;M<]:*VtlonGJnp )U1zGB ptZ^iz5b-|2{k!; o'v&zXC i‰Ch"mRɧZ|%'~ 8#FC6g\3ѐiP ."x8 ]D\%nO(~ya= &Hjgd.pr<)-tm#̪ M3ݛGA:"ݻ]p?޻Wv GFWT5'8Pc`_#1Zb:tu>w}XYr Y9HkJp:w€?4Tǥ`x]z4U Y6}`7-;Xj v$L U!{l3Qh:#[hB!c?-jH{"mNLR #\=B,Ov,c.|Xlҟ#=ڬ5 9t87C Kp!l% 32c$L;zY"qJ+'+("rRz{zOg@C)ފ@9$2R4j{k%&O0 S; 8^ CP8ʴ^Gzhyp#!rXor҉.xj?p=~&-hNSc2NѻN(wwS\?K\gzntxqĩNNMjLڹ:4dj8TXp*Uyxn3׵@/y-6ӥSkeJby5/-K&^ @ |1s3RN^d{@LAJ2fBJBچBc} :im `m qъqC1 Ȑ⬠~rr4m6vDHjI獱=N&WK;xmhN7B7+yf ui:^ѶFsB8̻Q 璱Gc]5L:뚏h͊AmKhb;PG1Ѷ# m;֎W|\[6"w3R!N8N+4RhZHY*D߹{+%PU8SpDOr}wxPTsbNV!s%3ەR:)x[aȭ#g杁z[H}\/ N)❠x2l<ĊVlލC!Q x4*. ހy}XVV;fq-DI^[DjLJ u[Y;+S߾Wi&H9pܮ3sR ͡ Byj&ww]p?19Ϥ!"heԑVȵޒGӊ0 cd"yl y|T>?l<7W*, 0l",[f>y 惹#vFJ <]h](xT"Ϗm׎:klWscEH^q4BN\s2f\jj]{~+iq۸_X~&:Z* !8VS@c`u,;A) -(W1zBYpM @a3NzKH)l'v̻6>=4^l}y`7EHA7qh׮;.OF ln1j1@6׻ -'k΀BU%[-x !L8ZO+q3ZW-e 0͹PIi9HtF\?t0&'t"fA#7D+M h>ikmH> 4nGBkN'Ǟ"Wv:5dMo~$@fd:0˜VżxڊeW{tȥ\ny͓m.h_<ôÇs4) e{ (*g kn Bu O?ו\H0:] t{̔5fF˕;y+ PWޝvM㏹])rog*صYs ΋;a -~9VNSqtf;O,1p:,s$871\ťD#4M>.7{.?5!.1WRI)F^/[ ?i7NJVYOQ턟 3#87|&@-3~ t6&eJHg& O2A"5oVlڛ&VW=wL^86$ω4 auN xx-8JE|!B9Lg&4+ [u V/- H]VmviK0-7ChAc/c kh"N'޼LɳGcwbް# RqiBKҲB&zv8ѻ0U\ZWBxl+BʜWfkx:V ]{D~ W'kf~py+޺ `>P[4c_I2Rr2NB9ѵb("a A7WqN0!i#ڊY֌xoOH.YxyvsH<9pxlG\:#XNvnЫqs䍋g3I!1 1 T:̐3xF9n e9Ȣt$kCS煛 'ccvnݨo$D/rK`͝e6(qLS|cĐw]pT fSq>G WeBD8+~Gk'iݸ2v#VFoDb fzKnuq+vbK0pT虦 cZkޛ.|Z5'A#n[ X||e7 zO6tfj}C2N!? {l$É'|f7U8K5 [+LѱDZ;UK ^@> CǧŬyj^Fz+nFZ=XZ.OWJ= .qi6x$igoqWsx)@B:o``|n3s/WaZ2 #L9BkV cx}LL1ytx=>?lx~v\g;Hs}ͦ56yucJq;ߜOZ6t:'N >ND3 zvE)K~GUWU#a)yslx_Y[Al 8!5[f!&U Lɸ65f38BoF5n.Ps 0bpo#eE拡su?Bb ZKB^yK[(2ml?.JFF&WmC)jr-oъ$j(a[ՈaQ$ СfylVwP7V%q]⺚B5Luʼn!ɑGп8|9?얷/Zxqmk)M@nO jYŚ,B+gZ8݄*DBnp!yJ 96f[ lыepNWa]7#mHS"NCb JOgB>ހG γ' igOK7n_\s~θ"k7i"ƶmLgl:a8Gbk~{c^Z{:nN$8n۴hl!(RP B" $!DXI/c!ؑ ہn{SUjus1ÇsSUZkyc/Q "{Z&R q2u?<.8T9rD\}8m#uhD\0#2-Aif"8uLhBU-Yg%8wheF֪Vm ݙ:tL_ e1;/vk W:0Z}v&kNja5ilհ'{jI֤ŴHF[s]1PZ(͋q܇|֮Ϳ<@ 8$hh-.Ps2M4[%nj't]L:+NFs/+2MC՟J_o~37|ڿ O8ܽaaMW,QUN:|Pc^cr oyšN*c !ir.t@.R˳xbcITׂ5qI=Sjԯ{]c9>[a إW:~~/yo{]y:O+U_=qMX `A;!5ϴٰ#828 matƫ$\Lj3^'= ial~ߟuozK0NyRl~+]Gܰ  [R΂()`@0X"sz 5K{s2tawZhU✵LrjEd@4e.aqHSAU({rczo8-aƨ>Z)8Rj1xw(V/#;!*d U~sJuK@MVckР4>vs,h퍠>!1(E{Né-K4p ˲yi5eu!-3TnG[g.(C9đ`^==u֗x:5tO=T (*օ_|e͞;k7nƉ{\ eYxsX w9Sק¨מ&깦߶J 54:w@ ôGZabqC dvjW언C0ޖ; PGj.PU @tVldKƝ'QJ>awݞٜ~!er'wIxEԚ.#-]DA݅fz-qӕQ&H4DÄ~[AOZ%[i@dހw"t T%7a@57?aO==T,G-]U LPQ6P:TUqxF }c7|BYo/sf:nG8*KUc\+M[fHړK4*<,V4 WO;Sԭ 5x!#j@ fFN.дQTU3)XnϭF96KpwCҗg@Q`30w?~7WW3Te'p CS|jaW*U=O /+oz)i3[iL'Z-i &*|`&#߾f+oPJʋˆCk&|m_l `?bA0xNBŒ+M6|  Zǁ5UZF/Џn˝U.yʼn3Jʅ+eYXJ3Û=9;ZY`[<ظ+ub8 S#nq3GPO۾y?u]ī?[M+K4 W˻#1,_:u1{R /OVkc]Pmo}=߈l~Y?{$ xS?ъNW?A^ꇉ_? 2Tg;ʚ :=h:4* OQ<}Q(8|]ܻ>pT[?^ L in6 @.-xJmj C/?r(0;|q(3Am=aZ1oD!F$8B4]61LF)e>%OKw_aJ<vCɼP o)J+)>X*.61r802/,T>|H^ߝneUϒ./7[^#Zp(),#5Y"Ud1E~hDbnuCWCgjku]h@\Z c<{r"g.Õ 9њRւlh8r*QN9Qm(bx_x[7;;fS3.Dj|>7o i$Z+.Lh*RCP 89,ׇ@dgKk:xꜙȣJV/4*ιNqTsP rT~e_W+wr9eLq5)BU!-'S܆8k#g5[Gcd3+ȺTr(L -5`@Y#8˗2v6I( w| ~C_L1ڱgf9͆vK:3Dcu%ٰ~}5_o)p>_Y";WOÇ~KjPJkP5ڠֶo$)5WqȇWȬoo50*շ#sVظ~u"Ϯ7N+BM(Ml.2aĺl8RvZ#3; 1(i>rH<dZ&UjTi kS(M]]UI[Ʒ)C)( q ;sh5Sn3PWmj{6Ku9kYTM滯ѻ6I#ߺj>#4BhxT~U=BsЩyfjyRjZrϵ!9YǑօf΄V;9S@˩['WTgr$ҝ[!t;`p|, W'A;`fP8Zq[i&BSR$sJ-lBx<, Qψ9?QYRS B-};j&|ƄFI3-쩚M9TDJkց"IZ%GB[0Pr-1)vs*{slw;~OI?sFJlH7z'a):fw3nAb:)Ex)9.Z51Ds4 Ks)xZ_5L־«u SBu[6 G@GZaGj5@^!xND.3i]x]]lK=pLXKcJ#"Qc N̶7F{i и3MVɥЀ18#u92f4ȥpaQZ8L8 @J.&8UeLZ@hIFZ,xrkOd*q:bmZM#zҊ ?ŭDH3gg?gA;08O.'DjD1!ZjvYZ3gk6&P MJ-iZ*5-(qCV;5smV_7Urb ic?,vB hTs8 a/eW({d9bthXre/fl Va3xޚpVaDz*#O)xZ_@CUjctg;=4ҪBLCP AxXWM!DH]Wf[ +9U6rZ3-7\ݦ1̧ъhzFjkLC`JI#B.?N]MBYNLZ 6/ UIM@k+$>el64*ksQ9-ΔV!@΅*MyKVIw_?#] J)+´fmw?T ΟpZ9& ,#c3DsV͆v-? RQV =GK &?h1AEH94M_-hΑK% #Fk'Dn֎o\ lBT8O'͖yYyP +9%r6]9 gCsF=ec^A{C7/CCzϳ)q|0,qcmɭQDX~8?OqyȴG?w B*6ڊ4^ wGŀu"|`ryƼnWRkoi=%Oy!Rmv.mIͦ9l b!^Tja) 8&iޝIP!T+y8fwe5W1w'#R %71V k ,Yix/-ʻnqĭ * ,Lf0π\p8q#J{ӄ8 *OJ++sU{b;>g~620:VU{@v3SA%#/&~ףݪrWJ$Ui&2$ĨBq''zahXSFr'^/6oe=JIws3]3sK!ơ/gYg}n?Uk/oYa49gaG%bg9W'~h5lF)(r{!yo n~giXyOSsێࢯvB?!_:cമ6S+N #iYF+7϶pHs<=(G(?O ҋRrW17Tȹ9\fɪ08`nf(Z j0/ Ē3cai<ʜ ֵrERTuODx>շFWo=ՅGRiLΜ̫;NǙ0mǁVWn(]0S%9$p"2l]z#bvXR&LkuaH^ 6FZJ%^6suj0i5N}k lWqE,Hߊ]ERI\C/!0 R fk3y' ÈVjMaxF=q'Rcw?|6?r9G$ml`XVKxJ z`7~]@<- ?Uks5hHꂵ#j37z3po_|l)}Oh\m <5> x _ߒ$J LhIv☕@f'Z. Cj{{uO)xZ_@ՆoGӑ"g!8uR+˚6©(ʼ R3^qѤW7ђ=md ~ Dgj~tK+&U\מqwXY[yGWM۠T ha ȰX )o)\cmXm9U^Цpݛp!jc P5xPe=wxv#%zs>%Hk=H5&ܠ끴ӖGM>MGܰi!^]-Vf[qTsԼ@:nI.iPMPڨGFJ_T7RMuR l7UB cG+h{52!f+j^7Y*&%\ nؠe2Pj-+SgV)n _}LW_7#rZ\_⹻T9)W5sjで)xZ_s~O#;_񛑥o*wsvnT8U0L_V֢<ۍ>.10xP/8mLk XŔ (z˫#q3Њb%8;PQۥNG&Xx~e[3ϟ_!-swaV0:zQ1FW^=$v-Ñx" ihˉUWۑ#ݖfY~.>Mh^&Sv w >/8mWk7U6Z*1Z-qcHbG#]1WÊ;V:[É{ig/'+,ϮFy:#x6{ܼ9Áim;[Z #JQJoۢ\GZ1h֡(Ύȫ9 *UˁV3.LZв%Z»ŏ2bd P6hS?Bwc% 8#L{#I8 8 VcvV~ W* 7wPP] q6>H͘Fkͨ!O)xZ_մ=CIKSJCUEilbsIwi."G\-l:Cz$)mdM>Gbt@3WS`"5'C0P S{nl'B\U~Ϩ  > ˒x6cdl[6x(.㩢ڨͱV}~2+w_+kNi*.<'q$K$7N:q.К2mwĶYiIUC! xQrD !PJGKZ ȯ~CC;|G~gbS<ZgYw +ք6Jrj5N+<03kf"RN355ݖRKfm8)-7fno@qÞNZ\˥FB+D a y{>j:v[Ĥy^0X3cȵҊD\Vh-n@]FVWyAűxmA^pn0Z^ *>Ns4 Ԃqi6jԅ0IM0LR:0a)R[FTXlj~ϷyD|y3hsNS^lH ǻf5s9*ޝP?o w|} o %7Yi])xZ_5Ni851|'m(>x1]6RP_sC6gW|xߛ;޺r⯽[Ϯm&R!-SHR62 !2#YIҾ>.,w7C~[¸z;F'k9m<シIyqų-)knvc %@ T|\pN\{|J,*Rr=:^:0S[HI3*qܰBntxM670@<eEǢV6J^Ѵ#mB8EJȥ>/~p a1v_΁f\unrL,ld#FvL՘M D]4LXB08a"+D]а[ Z[(w{\htR*&̕ 'hq=9^bf[K&BN"Ϲsx@~53 2^zYFDXׄY)] C0/QazL;@)&քn{-\DJNacVcƀ:瞔]ȥyOSāxw]f+/aD\mw/A(u4 8Ή#!8B$aYnxbZ+RFr ~@N l(,xi"S_TM"10kpwJš~LpwLCVЖY ƁZ C`;DˉWw'%1)kHm > C`͡Rzt2EFs3g QU[b-v ;/-E޹5B/{osCL̼s߁wǙgϮC_UJ ;BpP+l*8REbTU*f|Z%HJ&jIY-FZnM'X&RihSFWQJUgpݿZQ_Q07n8 a\ݼK&-Rsb۱N/ԸKux!EGh3wp1r4W^XLF"Р 9}W~/0z3x!lARurVO+q)rXp1 674irn@(|ga.4m|x2DnNf"˅Ή v jœ>T',Eќ ȝn+2B\8VJfUi4϶|RiKCÄ р7dw~;\ V0f'R(Mo=>8p(7;n{ByI,E;-|*V-qGuVnVemen֨{DDzjν;_N H(APJj**%Q; Z8ZZX&JhҨa䤮pW~Ry~eyy=dw<ΙT EyyK͔,rXLɩ 42hrSS ǵfW@ &p )IC_CWS0Mv=`ᮘv3a.81H. n3s< ~i5!!8Xւ'0O_*Kb "0#cqD4Fy:61FYDUk&q!u)q*CI )xZ_5 "/?m^Z QogN%.8Ca02Eaͅ uTX *{c!H=-ɹݱ2d5meYPP+y^ϕPW&.qiʴQ(TYlȫqA[BiZ8'I&i&R CB5Ys*1}e,+(M<8̅jnُ㚑|!5GJ5>qZ!8qp_XZ {n7 Q*KJ,j"dDabm+6\M՞ þ̱"(6bXR#(FT;? 5fjnR+JPX0!Wl҉" *9n3Cb.^a5,AnEYr"xv7F΍@1(Hq\o*?@e0 wL)+oG=wa=(kړ"SKCw!ӼL<84 Jhp}R)ݵrd ;l7y3\ŹqYQk0HrXmlqaHAD->| ϶Wwzx*y\)S;uB%1 NX֊w6ٍpZ>VLÿF is pZ 1x$DǕS0F1F}KUJZXUx,ka1Rُ9<1\ Gf"=ݝ {4/'IƁ0ܝMa=8RQLj>ͷaW 5zj=Xֆ5Y`knB39SKƉ✉3Ejc@':pHJb4J= |#k6fʋge!јƁVRVc̩2gnY}ʖ:'̥1WETX4v \rW~8 Q[mWީ焑p@Kb=cYO7 .O)xZ_+1K!|pykKEk9fWDž\k5^Pת8gaI@"p80'B 1vOHF t9@g>-DÂ9U#rWn HhZ(jTMrC8A9RW̆hU*a-VR iʜq>#B1=V1iQ{5?mT+x,U-w >sBbDk)W%!>kc3F!Sv"Ua@*)ڵ4/g4!)֕Wu));|\sh5^S?5E`qL%bv^u@DT^#ocQXrX*- @ G`[]W] V|3<[Z3(Bm0N959^Araj5bXJ%6Omp 53i8T*.xK)f|vR*`eM'4yP9ZeTpޛhM8-i,|`w=Rs8g28;jfN6ܔnSr@FXWTyX 7ǼBs҅YycF P:|~8s5+[,qu8m"fzʦe6|lnɂ= I)V*8 jwWƐTK4|U(k eTO`^ bI>#z!R k͜%'1P/3Yc-( ]$cK2$'W6g#R)nz.~aQL-n{YY1ZO raYDӰvC4Ԫq"|1(a"'[KbOպ*pb8Dw?-q6V T+RƁEaJUCD)1QTtJfpblJU5{ADXs4g A^L> J;PBfT "7{CrAE9b/Jʅ%kON "cD#޳L$ 4Rhqk6q"֖EL=Nْޭ"Bڔ"8d]iMx]y ΞhʒDpx">bre&$Lu*C3GJ >Dpޓ *k!475*J4P0hta^ )%qZG)W:MSV!XJ3gɵϛ*573N=G&xEgP )Ci{J},2O?7㸑FoZ :V Y/`3"lY5]F_rhU())C 69@RCm0 j- Τqq%x\wkߔR;:ق]e.suy6%J Ğ*I+zH(,תy,ͪ[!dUBSDBT'䴲&lfe3q!x(U-y*MHԁ.n;\8,TʏL C03X  LιV%ejJv1-UkA8O;qWZ8;r$QRix\DдƁe96nRbO)xZ_% k'yW16jW51ńSZ- ZS\6΢4f`JLf4R6u:]jmҺ܉ TjbjJ)JhŎ={vkNz47ͤxi- 9'Ji:g)bmoD=V g MłwW&d ͆DNM5ê|W D)Q9* ־=!rͺfMiym1i5zZ*851mfPwF>pTLF$J*\J;q)Mi XF.vwlh&#Fl5#ZbW f9C12XDžP1cKٮjIb(UMͯj&Fs~&ɏfdjo;&{1zNEэIO̿<-cvS$yhNii0MLPn2VZ! 2J <o3A]Mj Cx d\4<[V,hV8JiWg]\ t'Z.X Q f~ы%񣢻ٟ= vWrmYv 0 Ǝ8m'Pu 3*7Z >\#VF\O茲T9 EM By7YZb@fz.яmŒZ6jIz\swl)Ry,[=Kh.>Km "vKn^W#wM54Km},dI iM6^Y9R,yl=jj6wZkUS.BKkWu>Fs8!ƭ*Ъ9kg ZKxDL@ Q 6~,O/ T(Vq .gʳ#=1{hӺޖnwxO:WVwCpbsr;Pk .tNH,ikqN-z}.j8rD{2&Uy YiNѹsmFϥ띎ss7: ځ\Kb q+|K5ΘЏtg +ʠ\0]=lb$+Cp/#wV] v>&蹋N{Y7ScY ^ǵd䂖Hfy:uB,W;WO)xZOiv{?uw0lwֺϕFevWNV NP(%SRi}C{@z#?=IuRu$`/:ߎx$.c.#x|| &\')|Kscғ1aЏ'"KδV/g\GuI$@&DšSebX1 JO@[\ HzjE50C0\ Mpywez1r{ZO -QE/ǟx=,l(@vڣj6v]TVoB)v|KUfUG?BI` A\Gp܌opʛ:љЛ#3at\<7slwXU௟?놿~r"fmO~mI'qsz[dДXN(H'Fzl:[ZZMnLUonU6QYCmcvY,-l0:y+F7yۻ]]|W~_>orO 3^<x|F|=A($ 8GAh&*UE29Gհ&I 5+saI67suhİv`$F褩ùƢp'ݯ֋:p|S{fûϞowި[[7U\kjbظɆZc`-6"qJA6\z >Z ~]l/$DyzʻSo;-MAZBigrijdXV8_[M>XUfJ "J֦ϵ>O?7ҋ8v00N˒yȎ\aY#ɟ/Fs~<}{Z}u❷M ө](d J|u@乫wK aԥ:|pҼcL) 耏uPnMRZa9oad 7W|ugnk/ϗާh>g=u>o@D}<<4O)妔ǟD6K!zH1Sr[vs8D1?>`bX}gצXC]y8e^K*ԯsu>g Qi''3mFjnRy 5`/ &q8Lc1ZK51p]Oa9Oݎ*Ñ\*7W{|p@'\.`3B)+I+]: PaU8Dę$]>!B F.xg_7ꃧSiv!OODw?_g7lU 18:+Z>!b2TcBba"ɥZHo/p-J"|t 1MڒLR K_h<NwpEn/~3َ8 U9q` R|0}e53YY%~A(Ɏ8L:XJᴚ&wke_gXVs_t 3gKl'?sVZ{/_or_2۔<ifӟVv*f%'!`C]Zؤ[[T)zn-L+yt‰R͖^C6H9aH{ 1ZMyi/5G*`iٌ#5Wۉ!)(՜qЪ~rpl7ue^sre^Wjqo? '\:TD< {RɴyWނq9ssj\w>_ lWbpޑ֕!30**'B }̈́'aN1*s6롃=ZW FnvN 벰F37 !RqZM.3Fb Y$zro  S>wOǞ٩cs]H?{ff5Z׵>_j֒߿/sG0N@IRJ|~1}DEɬqPD>D͖ZK]bFka 5r8v"{N\~$ \H%#αmpbҚ%왁rµRj%sf_ݺD!}KujlbƁqf?턵'=8_$x|spNS~o {ne?faJ) 9q%WxO E/ߌidJlsN}B`ʁ37Ma\2)%D\Ё!:!@rRLy9v\߉E53. !7RU.)K5p#J<Iن>?55\ Gx%;yj/]K!rz~ai.3D!pW{vS$ZI g&ɜsan[e_Q ~۵=GSGbOzaUYX)9R1àZm!Fz(2dz>\ӂH $)xZ_~}evGlsSj|wj5I_.tH)Cz7^=,`Pk}CUUu5+Zz,_A!RHlrv#?GU ބ`J6 ء*10(TFAS PwCC)6DaEgb!jg2%{KÑ (ǻh6窫.7GmL"BߔG+s=G| Z&\Y\(}nu]KQBH-B`΄ښyQ{je qs=(Q@l̋bĢf"zoYuQ5:%ЋGQRN\l cF9򞬰&`HDZ[BNl#|R@OY?>rS05p^OiIl6[J9MDqXcJ.%|`IIi}Y?>k_5rGI9f]{RɔZRj0 =.;Vko;GƲ,>fwc̪vW~^ =x8'̲&Yu~oj+Nϱ;-&ta5.)B}PY }OL`^VJ.qbsZqkb%8KmX]rvu#u}δh ڬ |K3ыb)ޡK' I%K7Lp%狑zN 5}ڸ8B@ʅA˵K"8;'ÒXVp]Ftas,kDTeK\XV3 kZVNm*Ԓگ{YK!j#Z,Վ1:fN %pr)sϺ7zOJ ]>_~@y~;xs}q'B sq:(ϭX? eHpթ7Grٺ <{KUܧ`ܣ^wtT}oVs082& ^Pk!|Z8W-؛|a-rJ0^*{\K5!@mvrLDKv֟].Қv|\ 0JƘo8Y2dǾu޼ȣŔXw,ult:8uүUݬ(u\?n|~ .BixӴh\qr.R:^f_ r?X<Ŧ)'}q\l6${Ƌb|nmoX2~3X#<2P#?ho P">s҃ t]@̽ Ǜ?i`B/gSsў\>;'D7D,3|>V˒9 URcax 흀@-N.ZǞۻ@)8lpi>Xjk|F1Ӟ5Hx\> 7;9+d味.: r4>Gk 3iu+}u\ybQ|<} ǤDyZ>wԌ|g\3; kJZ ?ϟvۧ=) xJ@O ?(q/Tv}GU`._yS綾`iRm](hZnbh 4}Bl۪:IRo?~R.sD\i[uܫ֢}䧛q&(3,Sg?S!?zx"*8 ԋa\f쪍{bξ^k΋΂Vj-Rk%Ʊ(>W){D%y9> pȝ70GUk7p~ϥ7^V/[J1fp]Z6R.AA1G~nv \sy4{:wt>kt}V 蟯|wSO+!R:|Ϊ[`@Gi}~yz=|6'k OM8NO@SJ 1"S[#0Huαz v|U1D"g1Z`!å==ہGXGo= .sq N\6K  ;PDpj8r)Y?U6v$ǹғ0M Y"`J&߫;xޒK`Ǿorzgc`Ccsڥ:O)uP]DD-r  ϑ1$b vEln+$hY0/t^\D Z5zo=ItH +;Z@D~P@`QOG}7xJ;Q<}}_gm-RL |L\U)/ҫX F$DD;ȥri+iX18!hVj),k3J+y%g\yZ{Z.ťoytYf='Av'.Eήo\VE}1:Vyo u=!Vw{rOj1vu5r𞵚΁L]X](Z%R-{"jPH[ʤZSS5%>ΒR+CܓaV: =@&充8D&>U-1Xb"w>XlޯOǂnK>gO)2V7ygig_׾?ߙR3Ԏzm3367-eeס_'֊8U 8lVZ6*fr.v/Qɵ7ޗ6ya Wg=v=bֿy΍@vQw8ȵZ߹]'aҳ]O ._LΊziI9帾w Z%hz#)N5]̮#˨{,iZVǧkF 0]I$e bnkq'lboW%E v_͟i[%}TᜡPfPtל ohg xu.x g8W_W_t*wK2OO/@~> $;o˛imo竽.4ژRr`wӮWuU=ٖ$Ԧ*()WN?: PoZoΆR:i^]}:vP\UUJ(NЬg\JևLUZîhr:9OavޠL(9Y BE&{bKfo~nsVwl:̜v%)4&)-F/\E,:(7Sr38fM ]ߠ6skM=(*4qRGӉ>A*UA]CF P:X/=z~>iݯ$ u"I+E:8jgkmO_O})>%I xZ?}dKWo;nNWV,Rz E閭(*S}9 =TAX%PzUޙ{uАB`7n3lR*MH h'yQY6u|GМwȥu8/Wr7tT #Fi]νw"i`+aʾS,0M S"I4 BB7验z BXк?#Wj'=-`IBCz5fWkMAEwW%Z=*1 =qAT![ >"ѤuA(%S< 9^r sTUq8TLaOQ*ڥ#0EGYbZΓ$iL!DֻP)n>xz_\iUl<җfgEW*q;5+%Ɇܘ /aQ [)'!u=]R՚uۗQa͖)M$޸ȉ2vAR)pq~&PJiCHky'qcPF#s7EH&̹He $e{o;.Fi[EguESij$oYᣣ:I&bZTQГ@(c}/V2g%**k*9kҚM+"D(-mX{r\QQJѵ=(dfV8[z{~u̯n@7w底OOm)$%$ZB` 9_.zΙetb]H_b݉z4b#M1Tefcu$3J&.3 H9ՂQ %+E;RVKWS28'mغ&1ukDş`e9׮EȔBt&D>8;%QRV#%雃T9HcPrk JI|Ϣ HQ88NhBkJq|ɖ:6ĘaG !e4xG>1Ն6䔘35 I[ yZWh0ǹc&$O଩ ."TQ+qzǦdJQ"t^Z'9]ce hTNtNcTRTƾ@v+{tWC8EDL(L$P){_ qXKe,5}u-,4F풰>PR/'L3ZQ6+ֱVE4ERi*[71 *ü̬t)^mZSJuC FIj;GTZߏHWh ]ף5.&J/>{[-1<}rqJbcʤtB4IŝNť 1屗?4{/Jzڲ$]c&HeHkB j@q=eK̠rjhbىlʎEc(qSH]+輈c9JOj+U ]9sl0e"jj=S3Z.3fF~p<֘2*{]F]tNd|)Pt) )"sh4uBĐ9 R߄ʉ% (O  C6{sW*k5US1n<ԕQ#d/OPa Gbamc&%rL̛|򈹅7K~+]RbGx~^37=xח gExSz)˥DuLq+M˓̛2O".x֛ Fԍ;)BnY7-K* eݨG$21ip^M%ch)'vwLE)6 2|աEn*mL O@ܦ֊@V8jAveO])B6t1SJO_tM=W/YV3kxz3bZ\o1a/f8NZ2mJ@?8N  .7W f5pjT|s5]1 `BBhMJ VM-|?y)S ʔ͛v޽%N!9 0)'I-}@ZUIqFAևBʒ陁!VDWy*g|D>|}rd^YgxDWS5ؙFD4)-YYY\U-Z=h{QYmĥ mKG<}p1 ~$媥swXV9_!R62h8%EHE(I$Pرp}/4s+{-c"/5qy״9p[gc929_-N9zȜ,.FK)R =?Op1osq1pn9k*j/yS[gK?K"]i##T֒T"5ŜL/VK#fkd e۲n{^x?X-糚g/09QX$)*9'RxG,dShmKQ1Te&f=yjyM8)S}$/9PE hemQ2ߛUG_ Yeh4ODfq(,WYn66=G1J|6-=~Dc4ϞSI,*Ã9.\656jJ ƠϿ|ryx6_LJx1g*[1??gU_ȓG̚DF) Z8,A| X#ֹYRnePڐb"xcfuڅț%}lVwRWFb{/>_ԕق/_s6QͼvAmE (W|OY5EYI ʉ8-Zf ES(vFo|#?}+`"N 7D%R F+r$ZYNTZԖ4IJuXD0_]Xu}xx6٣ VĐw>2ymyn嚬 Z\x(fԭ1ۖMq8V"$fV*(h!a8~K:(m/HAih5O/.Ym6,-M]6wUbʘ"$dNk9R5H !WVf#NH`uu;Bƻ#gMM di蓢{yTI]ϫvæw,"EǏ~O9h˷|v|6#̼1S,?l:W-_\mx8>R[ ƎIV*D͐u]c@>x.W+ZX*yrJ40o2>g:y?`ởijV'kC#)J1f&hHC{R > Z4( u32ͱӺʾ P̉7%X_8/phmGkk0$|*˓P*u0QϕQXuXuLc9y3QեS"錋~/W%":gZD`ag56&mKLŬ!FشWGr^Si#3]}o;Ǚ4j'ZX%;labnyyRLTu<3!e4E[!*Jp}H]Z+!)Iqf39EeXhXM{GȞ8*.X9o. >:Yw=WrÓ(X.$J.@Jkf Z(2/9u-cYAW~+-jI{^}KNΰ !2銈՗|u̫~1:t Oaiyya]3kfTU5*70Oi r! 9P[;6hI&B %ܞ9dNkJ?@>%* Qd!&*nsX.:\VYeԕc69Xo:Rjg|tm[dѬF̺sYl| &aL7ZvV5"~XCk\^Q[ͣ-{{y L1eZBr͢mC`HBlVg/X/ٜ%xwڴ8%úl1qBSTR1 cL8ߕqA*]su,)ZFQU }6ѳ޴\9E]eTedR.*zOyֆMϋy/bheP]Å(̛gz3kq>FZx-̣G4%&}MQ3Fm bFS7l|$ Wo^h֌Ǭ".F֛5)ɗZ[S⥐ nG<G_mZq]ww&`JʝcRLҊ5$WI_oZϦ>kHk,gzTKIskk)q2e؊O焘o\[jəo}G>Qk,s D׋_hF+9v=o^"^:\ Զj ?L "LdN>/f+YSsֲ\G$@BSJ*"޳^4uEmkRE,0mߩ0(}?fMe9i ]:Ǧy'־Uͼb&/3|<"䝣j:g3.5M3~IuY\XI[!ZUۢa4 m,Q_(=`tǵ$ڿ GՁ?w>߇ǁM/ImjzúYr!*fMEefżhCm-Y,!ºw MhwWWt>+<:GB|`uXmY1u=zjR8+OM˪cɣ%6u;OwT"gcu##ż'X,fҫ"HC&*C99u!c:6Ή%Vڢ+KbThI.u%I ?(UE>V 2Qyœ<xάt˶m{fFPEWIaTfVUo@&DHR뼸 VB*3kjsd E.zף""aljZlrE0}w9`'ƞs^<{"RZqf3󺢶~΢4c&eKzW$AilŬ轐P 54sTbsUG|1CxT|9F^<|1TbK.FQJdߣ2E6fq#d\JPL0cϠxEؓ<{Ji|^P޻Ab!xO۶ΑrffE"K B}-m/P">;J+| ] ƌ-)Ru!bRd>Eާb\duS'I]e]漶4/k.1G@{z0ۗ'e>p>@KoSls\(bNT.\Q`*m*{p Id}s[5)Fr>uhզ%\re<'LsܟG㛘G ]hlF;\4 1 dT{wDnK*lU*)B ZBLrIbE֌6(x b(%+;ֈ;J^#dQi(1L**3#4F\*6rI)#1 mחVU՚EE)rJuIT s H>fFCmDӇ,2NN0NJ{G\c ڃfByS}wWWW]֍b#[Pҿ9UGa,U6מC&bhWEg+ǒ{ z)ecTIzvֺTqJo na1$(cPUYb$Lem0 61$bP[E}Lpby=mį`oJ1<.آVk?9poq{Ӎǥ;;wrA@ a ÅZk-Ҩ)P`q Cplvc#l{w+u-@}(jE;mJ"2 <@֖SD(QZ#m+9'F=mHgYHcrG9?x4WsmFd(P h+dsK1$~0:b c2CdOmѫXz8%*"wω0Q <xJE?`j( w%p|^쏘?hVFr鏪QN 6 mE} 4">b]kŕݶωm@کmHcBYcl[Jw,bR,܃mЗ ;I%A)}E:39%tS"9ÏPcCiu7:<E@c}۪}Hg:)gLa!T[#EV m͘ IR9f$\<! =9c6;I=?c|]Sx)o$:}Ta j?n]$ ҶJE+F*C`JN*2V$(AJX Ɩ oA[YF0¾62R.pAdԦHͦ"۪^-B@*0T<6PJ@)%)5ZXAqF#r\d` !HnvO_}'(eVX$YJaJ`%8YE9^ym9R!CGaC2C(H8ZROɊyx! LZ>4R#*H6*ޗE%aAMSɔ@iD֌[%3aH4JR-6S0Ow(1*[KwO.Afc닅Uᢗؕ˨WQxFaUD˜JNʒ$>,3+#ULN1!J|՘VTPk&BTQ' 6X#\Jk2{M)c}TǸ!ƻ%d aK&%RAM*@)[AJ|\>!|uZϞ<~\\F.I=T FNKM[]~KߺZ-3/;GӚiJd⽮>~xf8Q.~QT*Ejk yK(A\Y2e[E 2gmklpI]h` ZBD̬h ZQaWǍXŪ ąUƁ?IF 3k,JbGe7d5B95JJ~}-V]ȍ R.Hb)(J=gٷ:|r HٿpqqpcR/`,M]SU99Hf.b>)k\je2i0֐1bZZtbtD$،)ʔr,e?$G>ba{ў)\K| ِ1af@E 0ӻPȄ6〈) b5y˓Qu?#mm1)}mڬsSUd!Љ]BELl="t&IbXzQq1+.Uo TԘ*仌 }{S`އڨ"f#a;@ږ12ˬyBl8)5/bO')aKuމ`UUMo9{MݼhsZ7S80s2V䒕@j+YČ C*Pa2]I:r . b2]&VV.$ޡ JB6ƴU*L~)vvϔ7 2@ dU \D)}x,j)LA9x JqDHX /I'V Bh-$-e,hc<ȰJťh19T!W r'PRA.)DN<2TƢ!k#{٣ "=ؘIC>")BE  ߚaLf_mh]wVBjBJ)a$5GB%K΢JX"1>eN  t)'=޴;t@[_!<)P)+CE%#G΋؎6eF-4I #.͜9$Trn'I mvtAymFY _z"bubٛ@*m!LB嫢5ZХ5+[1b`E6%) t$P$b ԌI3(;C"ӚoD՞2Goԁ @1.0O]4cNhZN)ЇD!QQdUq"1'(cPʢ.(9)rf>' kjMcE2\PM al`f "F6b[Ube[dZmڀA)h4FeBH8/ 1Q!HW^"F3Te[c+$ć!f89'ֽTROamXZ6YjaaMq9̙M/x]Q\b4Pnh\W yOtV$UepZ6sl$4)91hm)HʉjX+~r88X+ DwL*Ɂ&!?D<8 .A=Q;o@1)F/1PsN= D>DlSC(W681 U1kG@{ g=:/(5hY\h*nVԕ !ڶ%SD@HՅD8d m/׻juUڞ3 2ǭUQl Jt޽ Bv3FS/Ld9!4  ~fG'!]fm$ !+=u^ ^V)EBcsÅ vy+ϛS!pZ;G׶}UƈFw-3fYH^JP{VkQtnZB-^+\`!q!ju%"TԵLq-I0feD0(mH \L%SZI˹vu"79ߑ@GV>2Q) ^`dD:o٧#ZR}/h1n`ZS2n/E+-!};Vr=ĦYք@I{Eh^̡ ?&'Vc)&6`z'z#ۧ>%AX'$wU H59O#>h H۞q}y)?pQUiaܫ!5" tE\`9둼&+E l^;.h `QFKFAS ~4Xc0zhs!ouwTF@C @]S?1һd97bID6P -]R?y5o߾ ~91"1]l!9!b/֦LhI %y-. V}/_k)# $TT(*~3M5Z;Ja(SZJ>%EcFSwo)=\g=Sb0%>d>]PGbyh .TѰzZK[.λQUS.ź58i!0uS~15/Z^cRV+~ت*JF+"Td-jz8Vp! %H(EbI$@zlx/5XI٠/YH|1b|/yKEJJg !~֊&B ˷<}"{mv,ǁa9uo5,NE#]Ǔۻw5uI29'UMk~yO`yk˫1a!PJ"΃5 R&8Gm`JomEų >qT?GړDT`` @H]# s=JS` k( Kߋ[ZJPFĊ=6ho6%Z+6hS5%U]c6Vz}=!b&3YngD+@޴,m nDE.vr/$zzɓ'S"! +؍UMea8.ژ9]xoJRcѴTL|J0ʙb${$@O٫/ڤjÇHlQw{wW 3GV~I]7fW 6рtuIHqr>1QS_ĬxvI47-;eL8וvzc-6XbceN-A׮32Q oKob26E>(N f| ZF2yv~ NK}o~UsNh-JiJilU15,Π?~1_||s(wd/xaLi1`/b:U%22$I@S7MüU#QX۾WWTU5:6lCLXųXꦦ) Jc&g_b {wg:UdiB(ի/V|pfl292IZ17PUbk !iwm1xǑF+\q~tΣYS>hy#pѦmYm62a+ll51 4c3sYӐٳ'V x`s 3$-*PxWXy St4*L@O@Jrp_$cZSW()wӗ닚p( #̡ xպeiǪoxgŖ1fs䘙e'?)]J?TfÌe]qx`=| \X2WW+^_-iꚋŜld]{Ǧ݈(ւ@XCY6XhzzO>b[<CrK꜌2ֶ*ZFB LC4*}0`őO(Qst}/QFlyھ2BWU-v}/޾$aD#vU=`>/:I+Q4s?1JՆYSq6387r>fjM%UY,l1'/ѓ'hdpĂr - %:Avf6 t b@|Cc]~}A_a`B@/f?G۲sF*yh=Z)=zH4f3*[]a ,g2 b$$Վ9oXZ+HA %*IHDh] fX<]UuD^+;q^AAQ溑}E=k߼~-bN4u=J j mȯv %'!"T İgZ4#JΪ\ k+"9E?&:dTAC4D]RNޠ޾-A |y/S!#YRf1r@EObycŜ 9Q5yGjݺ #m_!">ad*Uͬ쳱&bi z黮Z "TWlF-q։=q?:``BMmo m׽0BkۖY`ًa>MEOWrJ,K k[J֌m1FBePeU1tAG:X jYmD \}u-ZǕ 5 1E֫2zODh=VZͬcVeaSĈJOjBP|>zz E ׻r u!$ ?a S~ 眃:u3kk1i6n}`e+F|l(cz+ALE m\Lc:*8W1TxR4u'LʄVXkZcqL|}1YS]ii+Gj-dAєϾnX D5Qa{/XplyoI )PQ7;~ߖt7x\γlH)5孏;-3hя(!ly`qKSJ^xq4*QXqh1zI2T`m ViІP8L >yl!?Az(#/RQm"H8x:c۴Ry3jI_U3;^v@#RŘ4E^)RQF b2B q| AmEeD/oϩTA .AZA\)b_ Mu)H9AxhiP/Q! ]520h9Q@Ź<>x'ZS0wW7+u~}:XHn pIݴ|mT12!pMygj癇=R xTP܍9`36qv?J9 1./E0$!Sev[U~IbSTPf/B39NPzRJPʔWn335*pι" ~ [>[u`w_}a-jM Jt^.L r8RMکb$J0MC^?mw=x'O@yi0-D1'>5FŐй~˕M]&>R7Q&2>~xL"0U5#R s}{ÛV޾jfoα˷|vDM@EFT~}}J= r8 +OEU:liaenϣt+!x(?w_T H`'`Z Um~>P+d!H,e|>ߒ<{~Rk.?zz`/>~p_w/_^!K<_iqp8_{nf˫Wc4Y|yG>o&_^핽?T0?}`NsgJ}O#>odٮk@Q>y{'tܱ*>_9r˫Wy)og9_gOOs~4>>{Si]+$6ffRq 5G,f ZśEnUShßfp JfC U6d+(KZ!d#|G!ٿQ*V׏X—-1ʝOY ;t rZ+,{$Zw6WR]XvSB ?*ŭPW5>߳osWORS!߱jŔ~UћzjCHa us+)/R)n w#'!ؗ{&%y1A}8d@q߻YJ"?%Ss݅o^n 7{g%=}D@ʏI}=uHC91cuEN &J.e[؟.mž><鈄:LJu/xgnﻧq1P1XNO5N}SWνc?58&98&8M΁`_>fߵ=r BpHA]$>5%Ӻ- wlwOoˁڱ1U8G SpDNA ± }Qƻ&:>1`Z'$?>6 ݟ> S^_|S+S;T_on{J`~$) 1)dwEy)O Sp1@Rվ@}lw1{ӻ>) P*8]'<{K~}o"r >dw{ S0wH d)x}}rplv<>mNMjT})O 1 8O| Nx~zuD]{j`W<}5GrJp*p0Ӛip"pjH)]&9W>b@c;N]J@Ŵ`JN=.^=26o!~uT@ɿϑp}iM A×]S*qQP%u8w܃hI-:nӚi3_wm+|]t@U)7p~UHBLAJWuB8X\iM /f:ӚiMkZӚ_@: L]NiMkZӚִyKO`ZӚִ5)ִ5iMkZS0iMkZӚִ`ZӚִ5iM 5iMkZӚiMkZӚִ5%Ӛִ5iMkJ5iMkZӚ֔LkZӚִ5)ִ5iMkZS0iMkZӚִ`ZӚִ5iM 5iMkZӚiMkZӚִ5%Ӛִ5iM t[j[IENDB`golang-github-labstack-echo-4.11.1/_fixture/index.html000066400000000000000000000001721445647333300226610ustar00rootroot00000000000000 Echo golang-github-labstack-echo-4.11.1/bind.go000066400000000000000000000247571445647333300203210ustar00rootroot00000000000000package echo import ( "encoding" "encoding/xml" "errors" "fmt" "net/http" "reflect" "strconv" "strings" ) type ( // Binder is the interface that wraps the Bind method. Binder interface { Bind(i interface{}, c Context) error } // DefaultBinder is the default implementation of the Binder interface. DefaultBinder struct{} // BindUnmarshaler is the interface used to wrap the UnmarshalParam method. // Types that don't implement this, but do implement encoding.TextUnmarshaler // will use that interface instead. BindUnmarshaler interface { // UnmarshalParam decodes and assigns a value from an form or query param. UnmarshalParam(param string) error } ) // BindPathParams binds path params to bindable object func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error { names := c.ParamNames() values := c.ParamValues() params := map[string][]string{} for i, name := range names { params[name] = []string{values[i]} } if err := b.bindData(i, params, "param"); err != nil { return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } return nil } // BindQueryParams binds query params to bindable object func (b *DefaultBinder) BindQueryParams(c Context, i interface{}) error { if err := b.bindData(i, c.QueryParams(), "query"); err != nil { return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } return nil } // BindBody binds request body contents to bindable object // NB: then binding forms take note that this implementation uses standard library form parsing // which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm // See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm // See MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) { req := c.Request() if req.ContentLength == 0 { return } ctype := req.Header.Get(HeaderContentType) switch { case strings.HasPrefix(ctype, MIMEApplicationJSON): if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil { switch err.(type) { case *HTTPError: return err default: return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } } case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML): if err = xml.NewDecoder(req.Body).Decode(i); err != nil { if ute, ok := err.(*xml.UnsupportedTypeError); ok { return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err) } else if se, ok := err.(*xml.SyntaxError); ok { return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error())).SetInternal(err) } return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm): params, err := c.FormParams() if err != nil { return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } if err = b.bindData(i, params, "form"); err != nil { return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } default: return ErrUnsupportedMediaType } return nil } // BindHeaders binds HTTP headers to a bindable object func (b *DefaultBinder) BindHeaders(c Context, i interface{}) error { if err := b.bindData(i, c.Request().Header, "header"); err != nil { return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } return nil } // Bind implements the `Binder#Bind` function. // Binding is done in following order: 1) path params; 2) query params; 3) request body. Each step COULD override previous // step binded values. For single source binding use their own methods BindBody, BindQueryParams, BindPathParams. func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) { if err := b.BindPathParams(c, i); err != nil { return err } // Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body. // For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues. // The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670) method := c.Request().Method if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead { if err = b.BindQueryParams(c, i); err != nil { return err } } return b.BindBody(c, i) } // bindData will bind data ONLY fields in destination struct that have EXPLICIT tag func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string) error { if destination == nil || len(data) == 0 { return nil } typ := reflect.TypeOf(destination).Elem() val := reflect.ValueOf(destination).Elem() // Map if typ.Kind() == reflect.Map { for k, v := range data { val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) } return nil } // !struct if typ.Kind() != reflect.Struct { if tag == "param" || tag == "query" || tag == "header" { // incompatible type, data is probably to be found in the body return nil } return errors.New("binding element must be a struct") } for i := 0; i < typ.NumField(); i++ { typeField := typ.Field(i) structField := val.Field(i) if typeField.Anonymous { if structField.Kind() == reflect.Ptr { structField = structField.Elem() } } if !structField.CanSet() { continue } structFieldKind := structField.Kind() inputFieldName := typeField.Tag.Get(tag) if typeField.Anonymous && structField.Kind() == reflect.Struct && inputFieldName != "" { // if anonymous struct with query/param/form tags, report an error return errors.New("query/param/form tags are not allowed with anonymous struct field") } if inputFieldName == "" { // If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags). // structs that implement BindUnmarshaler are binded only when they have explicit tag if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct { if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil { return err } } // does not have explicit tag and is not an ordinary struct - so move to next field continue } inputValue, exists := data[inputFieldName] if !exists { // Go json.Unmarshal supports case insensitive binding. However the // url params are bound case sensitive which is inconsistent. To // fix this we must check all of the map values in a // case-insensitive search. for k, v := range data { if strings.EqualFold(k, inputFieldName) { inputValue = v exists = true break } } } if !exists { continue } // Call this first, in case we're dealing with an alias to an array type if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok { if err != nil { return err } continue } numElems := len(inputValue) if structFieldKind == reflect.Slice && numElems > 0 { sliceOf := structField.Type().Elem().Kind() slice := reflect.MakeSlice(structField.Type(), numElems, numElems) for j := 0; j < numElems; j++ { if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil { return err } } val.Field(i).Set(slice) } else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { return err } } return nil } func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { // But also call it here, in case we're dealing with an array of BindUnmarshalers if ok, err := unmarshalField(valueKind, val, structField); ok { return err } switch valueKind { case reflect.Ptr: return setWithProperType(structField.Elem().Kind(), val, structField.Elem()) case reflect.Int: return setIntField(val, 0, structField) case reflect.Int8: return setIntField(val, 8, structField) case reflect.Int16: return setIntField(val, 16, structField) case reflect.Int32: return setIntField(val, 32, structField) case reflect.Int64: return setIntField(val, 64, structField) case reflect.Uint: return setUintField(val, 0, structField) case reflect.Uint8: return setUintField(val, 8, structField) case reflect.Uint16: return setUintField(val, 16, structField) case reflect.Uint32: return setUintField(val, 32, structField) case reflect.Uint64: return setUintField(val, 64, structField) case reflect.Bool: return setBoolField(val, structField) case reflect.Float32: return setFloatField(val, 32, structField) case reflect.Float64: return setFloatField(val, 64, structField) case reflect.String: structField.SetString(val) default: return errors.New("unknown type") } return nil } func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) { switch valueKind { case reflect.Ptr: return unmarshalFieldPtr(val, field) default: return unmarshalFieldNonPtr(val, field) } } func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) { fieldIValue := field.Addr().Interface() if unmarshaler, ok := fieldIValue.(BindUnmarshaler); ok { return true, unmarshaler.UnmarshalParam(value) } if unmarshaler, ok := fieldIValue.(encoding.TextUnmarshaler); ok { return true, unmarshaler.UnmarshalText([]byte(value)) } return false, nil } func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) { if field.IsNil() { // Initialize the pointer to a nil value field.Set(reflect.New(field.Type().Elem())) } return unmarshalFieldNonPtr(value, field.Elem()) } func setIntField(value string, bitSize int, field reflect.Value) error { if value == "" { value = "0" } intVal, err := strconv.ParseInt(value, 10, bitSize) if err == nil { field.SetInt(intVal) } return err } func setUintField(value string, bitSize int, field reflect.Value) error { if value == "" { value = "0" } uintVal, err := strconv.ParseUint(value, 10, bitSize) if err == nil { field.SetUint(uintVal) } return err } func setBoolField(value string, field reflect.Value) error { if value == "" { value = "false" } boolVal, err := strconv.ParseBool(value) if err == nil { field.SetBool(boolVal) } return err } func setFloatField(value string, bitSize int, field reflect.Value) error { if value == "" { value = "0.0" } floatVal, err := strconv.ParseFloat(value, bitSize) if err == nil { field.SetFloat(floatVal) } return err } golang-github-labstack-echo-4.11.1/bind_test.go000066400000000000000000001030621445647333300213430ustar00rootroot00000000000000package echo import ( "bytes" "encoding/json" "encoding/xml" "errors" "io" "mime/multipart" "net/http" "net/http/httptest" "net/url" "reflect" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) type ( bindTestStruct struct { I int PtrI *int I8 int8 PtrI8 *int8 I16 int16 PtrI16 *int16 I32 int32 PtrI32 *int32 I64 int64 PtrI64 *int64 UI uint PtrUI *uint UI8 uint8 PtrUI8 *uint8 UI16 uint16 PtrUI16 *uint16 UI32 uint32 PtrUI32 *uint32 UI64 uint64 PtrUI64 *uint64 B bool PtrB *bool F32 float32 PtrF32 *float32 F64 float64 PtrF64 *float64 S string PtrS *string cantSet string DoesntExist string GoT time.Time GoTptr *time.Time T Timestamp Tptr *Timestamp SA StringArray } bindTestStructWithTags struct { I int `json:"I" form:"I"` PtrI *int `json:"PtrI" form:"PtrI"` I8 int8 `json:"I8" form:"I8"` PtrI8 *int8 `json:"PtrI8" form:"PtrI8"` I16 int16 `json:"I16" form:"I16"` PtrI16 *int16 `json:"PtrI16" form:"PtrI16"` I32 int32 `json:"I32" form:"I32"` PtrI32 *int32 `json:"PtrI32" form:"PtrI32"` I64 int64 `json:"I64" form:"I64"` PtrI64 *int64 `json:"PtrI64" form:"PtrI64"` UI uint `json:"UI" form:"UI"` PtrUI *uint `json:"PtrUI" form:"PtrUI"` UI8 uint8 `json:"UI8" form:"UI8"` PtrUI8 *uint8 `json:"PtrUI8" form:"PtrUI8"` UI16 uint16 `json:"UI16" form:"UI16"` PtrUI16 *uint16 `json:"PtrUI16" form:"PtrUI16"` UI32 uint32 `json:"UI32" form:"UI32"` PtrUI32 *uint32 `json:"PtrUI32" form:"PtrUI32"` UI64 uint64 `json:"UI64" form:"UI64"` PtrUI64 *uint64 `json:"PtrUI64" form:"PtrUI64"` B bool `json:"B" form:"B"` PtrB *bool `json:"PtrB" form:"PtrB"` F32 float32 `json:"F32" form:"F32"` PtrF32 *float32 `json:"PtrF32" form:"PtrF32"` F64 float64 `json:"F64" form:"F64"` PtrF64 *float64 `json:"PtrF64" form:"PtrF64"` S string `json:"S" form:"S"` PtrS *string `json:"PtrS" form:"PtrS"` cantSet string DoesntExist string `json:"DoesntExist" form:"DoesntExist"` GoT time.Time `json:"GoT" form:"GoT"` GoTptr *time.Time `json:"GoTptr" form:"GoTptr"` T Timestamp `json:"T" form:"T"` Tptr *Timestamp `json:"Tptr" form:"Tptr"` SA StringArray `json:"SA" form:"SA"` } Timestamp time.Time TA []Timestamp StringArray []string Struct struct { Foo string } Bar struct { Baz int `json:"baz" query:"baz"` } ) func (t *Timestamp) UnmarshalParam(src string) error { ts, err := time.Parse(time.RFC3339, src) *t = Timestamp(ts) return err } func (a *StringArray) UnmarshalParam(src string) error { *a = StringArray(strings.Split(src, ",")) return nil } func (s *Struct) UnmarshalParam(src string) error { *s = Struct{ Foo: src, } return nil } func (t bindTestStruct) GetCantSet() string { return t.cantSet } var values = map[string][]string{ "I": {"0"}, "PtrI": {"0"}, "I8": {"8"}, "PtrI8": {"8"}, "I16": {"16"}, "PtrI16": {"16"}, "I32": {"32"}, "PtrI32": {"32"}, "I64": {"64"}, "PtrI64": {"64"}, "UI": {"0"}, "PtrUI": {"0"}, "UI8": {"8"}, "PtrUI8": {"8"}, "UI16": {"16"}, "PtrUI16": {"16"}, "UI32": {"32"}, "PtrUI32": {"32"}, "UI64": {"64"}, "PtrUI64": {"64"}, "B": {"true"}, "PtrB": {"true"}, "F32": {"32.5"}, "PtrF32": {"32.5"}, "F64": {"64.5"}, "PtrF64": {"64.5"}, "S": {"test"}, "PtrS": {"test"}, "cantSet": {"test"}, "T": {"2016-12-06T19:09:05+01:00"}, "Tptr": {"2016-12-06T19:09:05+01:00"}, "GoT": {"2016-12-06T19:09:05+01:00"}, "GoTptr": {"2016-12-06T19:09:05+01:00"}, "ST": {"bar"}, } func TestToMultipleFields(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?id=1&ID=2", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) type Root struct { ID int64 `query:"id"` Child2 struct { ID int64 } Child1 struct { ID int64 `query:"id"` } } u := new(Root) err := c.Bind(u) if assert.NoError(t, err) { assert.Equal(t, int64(1), u.ID) // perfectly reasonable assert.Equal(t, int64(1), u.Child1.ID) // untagged struct containing tagged field gets filled (by tag) assert.Equal(t, int64(0), u.Child2.ID) // untagged struct containing untagged field should not be bind } } func TestBindJSON(t *testing.T) { testBindOkay(t, strings.NewReader(userJSON), nil, MIMEApplicationJSON) testBindOkay(t, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON) testBindArrayOkay(t, strings.NewReader(usersJSON), nil, MIMEApplicationJSON) testBindArrayOkay(t, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON) testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{}) testBindError(t, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{}) } func TestBindXML(t *testing.T) { testBindOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML) testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML) testBindArrayOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML) testBindArrayOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML) testBindError(t, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New("")) testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{}) testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{}) testBindOkay(t, strings.NewReader(userXML), nil, MIMETextXML) testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMETextXML) testBindError(t, strings.NewReader(invalidContent), MIMETextXML, errors.New("")) testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{}) testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{}) } func TestBindForm(t *testing.T) { testBindOkay(t, strings.NewReader(userForm), nil, MIMEApplicationForm) testBindOkay(t, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm) e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userForm)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(HeaderContentType, MIMEApplicationForm) err := c.Bind(&[]struct{ Field string }{}) assert.Error(t, err) } func TestBindQueryParams(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?id=1&name=Jon+Snow", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := c.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Snow", u.Name) } } func TestBindQueryParamsCaseInsensitive(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?ID=1&NAME=Jon+Snow", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := c.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Snow", u.Name) } } func TestBindQueryParamsCaseSensitivePrioritized(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?id=1&ID=2&NAME=Jon+Snow&name=Jon+Doe", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := c.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Doe", u.Name) } } func TestBindHeaderParam(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Name", "Jon Doe") req.Header.Set("Id", "2") rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := (&DefaultBinder{}).BindHeaders(c, u) if assert.NoError(t, err) { assert.Equal(t, 2, u.ID) assert.Equal(t, "Jon Doe", u.Name) } } func TestBindHeaderParamBadType(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Id", "salamander") rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := (&DefaultBinder{}).BindHeaders(c, u) assert.Error(t, err) httpErr, ok := err.(*HTTPError) if assert.True(t, ok) { assert.Equal(t, http.StatusBadRequest, httpErr.Code) } } func TestBindUnmarshalParam(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { T Timestamp `query:"ts"` TA []Timestamp `query:"ta"` SA StringArray `query:"sa"` ST Struct StWithTag struct { Foo string `query:"st"` } }{} err := c.Bind(&result) ts := Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)) if assert.NoError(t, err) { // assert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T) assert.Equal(t, ts, result.T) assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA) assert.Equal(t, []Timestamp{ts, ts}, result.TA) assert.Equal(t, Struct{""}, result.ST) // child struct does not have a field with matching tag assert.Equal(t, "baz", result.StWithTag.Foo) // child struct has field with matching tag } } func TestBindUnmarshalText(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { T time.Time `query:"ts"` TA []time.Time `query:"ta"` SA StringArray `query:"sa"` ST Struct }{} err := c.Bind(&result) ts := time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC) if assert.NoError(t, err) { // assert.Equal(t, Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T) assert.Equal(t, ts, result.T) assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA) assert.Equal(t, []time.Time{ts, ts}, result.TA) assert.Equal(t, Struct{""}, result.ST) // field in child struct does not have tag } } func TestBindUnmarshalParamPtr(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { Tptr *Timestamp `query:"ts"` }{} err := c.Bind(&result) if assert.NoError(t, err) { assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), *result.Tptr) } } func TestBindUnmarshalParamAnonymousFieldPtr(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { *Bar }{&Bar{}} err := c.Bind(&result) if assert.NoError(t, err) { assert.Equal(t, 1, result.Baz) } } func TestBindUnmarshalParamAnonymousFieldPtrNil(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { *Bar }{} err := c.Bind(&result) if assert.NoError(t, err) { assert.Nil(t, result.Bar) } } func TestBindUnmarshalParamAnonymousFieldPtrCustomTag(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, `/?bar={"baz":100}&baz=1`, nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { *Bar `json:"bar" query:"bar"` }{&Bar{}} err := c.Bind(&result) assert.Contains(t, err.Error(), "query/param/form tags are not allowed with anonymous struct field") } func TestBindUnmarshalTextPtr(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) result := struct { Tptr *time.Time `query:"ts"` }{} err := c.Bind(&result) if assert.NoError(t, err) { assert.Equal(t, time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC), *result.Tptr) } } func TestBindMultipartForm(t *testing.T) { bodyBuffer := new(bytes.Buffer) mw := multipart.NewWriter(bodyBuffer) mw.WriteField("id", "1") mw.WriteField("name", "Jon Snow") mw.Close() body := bodyBuffer.Bytes() testBindOkay(t, bytes.NewReader(body), nil, mw.FormDataContentType()) testBindOkay(t, bytes.NewReader(body), dummyQuery, mw.FormDataContentType()) } func TestBindUnsupportedMediaType(t *testing.T) { testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{}) } func TestBindbindData(t *testing.T) { ts := new(bindTestStruct) b := new(DefaultBinder) err := b.bindData(ts, values, "form") assert.NoError(t, err) assert.Equal(t, 0, ts.I) assert.Equal(t, int8(0), ts.I8) assert.Equal(t, int16(0), ts.I16) assert.Equal(t, int32(0), ts.I32) assert.Equal(t, int64(0), ts.I64) assert.Equal(t, uint(0), ts.UI) assert.Equal(t, uint8(0), ts.UI8) assert.Equal(t, uint16(0), ts.UI16) assert.Equal(t, uint32(0), ts.UI32) assert.Equal(t, uint64(0), ts.UI64) assert.Equal(t, false, ts.B) assert.Equal(t, float32(0), ts.F32) assert.Equal(t, float64(0), ts.F64) assert.Equal(t, "", ts.S) assert.Equal(t, "", ts.cantSet) } func TestBindParam(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetPath("/users/:id/:name") c.SetParamNames("id", "name") c.SetParamValues("1", "Jon Snow") u := new(user) err := c.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Snow", u.Name) } // Second test for the absence of a param c2 := e.NewContext(req, rec) c2.SetPath("/users/:id") c2.SetParamNames("id") c2.SetParamValues("1") u = new(user) err = c2.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "", u.Name) } // Bind something with param and post data payload body := bytes.NewBufferString(`{ "name": "Jon Snow" }`) e2 := New() req2 := httptest.NewRequest(http.MethodPost, "/", body) req2.Header.Set(HeaderContentType, MIMEApplicationJSON) rec2 := httptest.NewRecorder() c3 := e2.NewContext(req2, rec2) c3.SetPath("/users/:id") c3.SetParamNames("id") c3.SetParamValues("1") u = new(user) err = c3.Bind(u) if assert.NoError(t, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Snow", u.Name) } } func TestBindUnmarshalTypeError(t *testing.T) { body := bytes.NewBufferString(`{ "id": "text" }`) e := New() req := httptest.NewRequest(http.MethodPost, "/", body) req.Header.Set(HeaderContentType, MIMEApplicationJSON) rec := httptest.NewRecorder() c := e.NewContext(req, rec) u := new(user) err := c.Bind(u) he := &HTTPError{Code: http.StatusBadRequest, Message: "Unmarshal type error: expected=int, got=string, field=id, offset=14", Internal: err.(*HTTPError).Internal} assert.Equal(t, he, err) } func TestBindSetWithProperType(t *testing.T) { ts := new(bindTestStruct) typ := reflect.TypeOf(ts).Elem() val := reflect.ValueOf(ts).Elem() for i := 0; i < typ.NumField(); i++ { typeField := typ.Field(i) structField := val.Field(i) if !structField.CanSet() { continue } if len(values[typeField.Name]) == 0 { continue } val := values[typeField.Name][0] err := setWithProperType(typeField.Type.Kind(), val, structField) assert.NoError(t, err) } assertBindTestStruct(t, ts) type foo struct { Bar bytes.Buffer } v := &foo{} typ = reflect.TypeOf(v).Elem() val = reflect.ValueOf(v).Elem() assert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0))) } func TestBindSetFields(t *testing.T) { ts := new(bindTestStruct) val := reflect.ValueOf(ts).Elem() // Int if assert.NoError(t, setIntField("5", 0, val.FieldByName("I"))) { assert.Equal(t, 5, ts.I) } if assert.NoError(t, setIntField("", 0, val.FieldByName("I"))) { assert.Equal(t, 0, ts.I) } // Uint if assert.NoError(t, setUintField("10", 0, val.FieldByName("UI"))) { assert.Equal(t, uint(10), ts.UI) } if assert.NoError(t, setUintField("", 0, val.FieldByName("UI"))) { assert.Equal(t, uint(0), ts.UI) } // Float if assert.NoError(t, setFloatField("15.5", 0, val.FieldByName("F32"))) { assert.Equal(t, float32(15.5), ts.F32) } if assert.NoError(t, setFloatField("", 0, val.FieldByName("F32"))) { assert.Equal(t, float32(0.0), ts.F32) } // Bool if assert.NoError(t, setBoolField("true", val.FieldByName("B"))) { assert.Equal(t, true, ts.B) } if assert.NoError(t, setBoolField("", val.FieldByName("B"))) { assert.Equal(t, false, ts.B) } ok, err := unmarshalFieldNonPtr("2016-12-06T19:09:05Z", val.FieldByName("T")) if assert.NoError(t, err) { assert.Equal(t, ok, true) assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), ts.T) } } func BenchmarkBindbindDataWithTags(b *testing.B) { b.ReportAllocs() ts := new(bindTestStructWithTags) binder := new(DefaultBinder) var err error b.ResetTimer() for i := 0; i < b.N; i++ { err = binder.bindData(ts, values, "form") } assert.NoError(b, err) assertBindTestStruct(b, (*bindTestStruct)(ts)) } func assertBindTestStruct(tb testing.TB, ts *bindTestStruct) { assert.Equal(tb, 0, ts.I) assert.Equal(tb, int8(8), ts.I8) assert.Equal(tb, int16(16), ts.I16) assert.Equal(tb, int32(32), ts.I32) assert.Equal(tb, int64(64), ts.I64) assert.Equal(tb, uint(0), ts.UI) assert.Equal(tb, uint8(8), ts.UI8) assert.Equal(tb, uint16(16), ts.UI16) assert.Equal(tb, uint32(32), ts.UI32) assert.Equal(tb, uint64(64), ts.UI64) assert.Equal(tb, true, ts.B) assert.Equal(tb, float32(32.5), ts.F32) assert.Equal(tb, float64(64.5), ts.F64) assert.Equal(tb, "test", ts.S) assert.Equal(tb, "", ts.GetCantSet()) } func testBindOkay(t *testing.T, r io.Reader, query url.Values, ctype string) { e := New() path := "/" if len(query) > 0 { path += "?" + query.Encode() } req := httptest.NewRequest(http.MethodPost, path, r) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(HeaderContentType, ctype) u := new(user) err := c.Bind(u) if assert.Equal(t, nil, err) { assert.Equal(t, 1, u.ID) assert.Equal(t, "Jon Snow", u.Name) } } func testBindArrayOkay(t *testing.T, r io.Reader, query url.Values, ctype string) { e := New() path := "/" if len(query) > 0 { path += "?" + query.Encode() } req := httptest.NewRequest(http.MethodPost, path, r) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(HeaderContentType, ctype) u := []user{} err := c.Bind(&u) if assert.NoError(t, err) { assert.Equal(t, 1, len(u)) assert.Equal(t, 1, u[0].ID) assert.Equal(t, "Jon Snow", u[0].Name) } } func testBindError(t *testing.T, r io.Reader, ctype string, expectedInternal error) { e := New() req := httptest.NewRequest(http.MethodPost, "/", r) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(HeaderContentType, ctype) u := new(user) err := c.Bind(u) switch { case strings.HasPrefix(ctype, MIMEApplicationJSON), strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML), strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm): if assert.IsType(t, new(HTTPError), err) { assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code) assert.IsType(t, expectedInternal, err.(*HTTPError).Internal) } default: if assert.IsType(t, new(HTTPError), err) { assert.Equal(t, ErrUnsupportedMediaType, err) assert.IsType(t, expectedInternal, err.(*HTTPError).Internal) } } } func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) { // tests to check binding behaviour when multiple sources path params, query params and request body are in use // binding is done in steps and one source could overwrite previous source binded data // these tests are to document this behaviour and detect further possible regressions when bind implementation is changed type Opts struct { ID int `json:"id" form:"id" query:"id"` Node string `json:"node" form:"node" query:"node" param:"node"` Lang string } var testCases = []struct { name string givenURL string givenContent io.Reader givenMethod string whenBindTarget interface{} whenNoPathParams bool expect interface{} expectError string }{ { name: "ok, POST bind to struct with: path param + query param + body", givenMethod: http.MethodPost, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1}`), expect: &Opts{ID: 1, Node: "node_from_path"}, // query params are not used, node is filled from path }, { name: "ok, PUT bind to struct with: path param + query param + body", givenMethod: http.MethodPut, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1}`), expect: &Opts{ID: 1, Node: "node_from_path"}, // query params are not used }, { name: "ok, GET bind to struct with: path param + query param + body", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1}`), expect: &Opts{ID: 1, Node: "xxx"}, // query overwrites previous path value }, { name: "ok, GET bind to struct with: path param + query param + body", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Opts{ID: 1, Node: "zzz"}, // body is binded last and overwrites previous (path,query) values }, { name: "ok, DELETE bind to struct with: path param + query param + body", givenMethod: http.MethodDelete, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Opts{ID: 1, Node: "zzz"}, // for DELETE body is binded after query params }, { name: "ok, POST bind to struct with: path param + body", givenMethod: http.MethodPost, givenURL: "/api/real_node/endpoint", givenContent: strings.NewReader(`{"id": 1}`), expect: &Opts{ID: 1, Node: "node_from_path"}, }, { name: "ok, POST bind to struct with path + query + body = body has priority", givenMethod: http.MethodPost, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Opts{ID: 1, Node: "zzz"}, // field value from content has higher priority }, { name: "nok, POST body bind failure", givenMethod: http.MethodPost, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`{`), expect: &Opts{ID: 0, Node: "node_from_path"}, // query binding has already modified bind target expectError: "code=400, message=unexpected EOF, internal=unexpected EOF", }, { name: "nok, GET with body bind failure when types are not convertible", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?id=nope", givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Opts{ID: 0, Node: "node_from_path"}, // path params binding has already modified bind target expectError: "code=400, message=strconv.ParseInt: parsing \"nope\": invalid syntax, internal=strconv.ParseInt: parsing \"nope\": invalid syntax", }, { name: "nok, GET body bind failure - trying to bind json array to struct", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), expect: &Opts{ID: 0, Node: "xxx"}, // query binding has already modified bind target expectError: "code=400, message=Unmarshal type error: expected=echo.Opts, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Opts", }, { // query param is ignored as we do not know where exactly to bind it in slice name: "ok, GET bind to struct slice, ignore query param", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Opts{}, expect: &[]Opts{ {ID: 1, Node: ""}, }, }, { // binding query params interferes with body. b.BindBody() should be used to bind only body to slice name: "ok, POST binding to slice should not be affected query params types", givenMethod: http.MethodPost, givenURL: "/api/real_node/endpoint?id=nope&node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Opts{}, expect: &[]Opts{{ID: 1}}, expectError: "", }, { // path param is ignored as we do not know where exactly to bind it in slice name: "ok, GET bind to struct slice, ignore path param", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenBindTarget: &[]Opts{}, expect: &[]Opts{ {ID: 1, Node: ""}, }, }, { name: "ok, GET body bind json array to slice", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint", givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Opts{}, expect: &[]Opts{{ID: 1, Node: ""}}, expectError: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() // assume route we are testing is "/api/:node/endpoint?some_query_params=here" req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent) req.Header.Set(HeaderContentType, MIMEApplicationJSON) rec := httptest.NewRecorder() c := e.NewContext(req, rec) if !tc.whenNoPathParams { c.SetParamNames("node") c.SetParamValues("node_from_path") } var bindTarget interface{} if tc.whenBindTarget != nil { bindTarget = tc.whenBindTarget } else { bindTarget = &Opts{} } b := new(DefaultBinder) err := b.Bind(bindTarget, c) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.Equal(t, tc.expect, bindTarget) }) } } func TestDefaultBinder_BindBody(t *testing.T) { // tests to check binding behaviour when multiple sources path params, query params and request body are in use // generally when binding from request body - URL and path params are ignored - unless form is being binded. // these tests are to document this behaviour and detect further possible regressions when bind implementation is changed type Node struct { ID int `json:"id" xml:"id" form:"id" query:"id"` Node string `json:"node" xml:"node" form:"node" query:"node" param:"node"` } type Nodes struct { Nodes []Node `xml:"node" form:"node"` } var testCases = []struct { name string givenURL string givenContent io.Reader givenMethod string givenContentType string whenNoPathParams bool whenBindTarget interface{} expect interface{} expectError string }{ { name: "ok, JSON POST bind to struct with: path + query + empty field in body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`{"id": 1}`), expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body }, { name: "ok, JSON POST bind to struct with: path + query + body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority }, { name: "ok, JSON POST body bind json array to slice (has matching path/query params)", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Node{}, expect: &[]Node{{ID: 1, Node: ""}}, expectError: "", }, { // rare case as GET is not usually used to send request body name: "ok, JSON GET bind to struct with: path + query + empty field in body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodGet, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`{"id": 1}`), expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body }, { // rare case as GET is not usually used to send request body name: "ok, JSON GET bind to struct with: path + query + body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodGet, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`), expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority }, { name: "nok, JSON POST body bind failure", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationJSON, givenContent: strings.NewReader(`{`), expect: &Node{ID: 0, Node: ""}, expectError: "code=400, message=unexpected EOF, internal=unexpected EOF", }, { name: "ok, XML POST bind to struct with: path + query + empty body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationXML, givenContent: strings.NewReader(`1yyy`), expect: &Node{ID: 1, Node: "yyy"}, }, { name: "ok, XML POST bind array to slice with: path + query + body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationXML, givenContent: strings.NewReader(`1yyy`), whenBindTarget: &Nodes{}, expect: &Nodes{Nodes: []Node{{ID: 1, Node: "yyy"}}}, }, { name: "nok, XML POST bind failure", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationXML, givenContent: strings.NewReader(`<`), expect: &Node{ID: 0, Node: ""}, expectError: "code=400, message=Syntax error: line=1, error=XML syntax error on line 1: unexpected EOF, internal=XML syntax error on line 1: unexpected EOF", }, { name: "ok, FORM POST bind to struct with: path + query + body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationForm, givenContent: strings.NewReader(`id=1&node=yyy`), expect: &Node{ID: 1, Node: "yyy"}, }, { // NB: form values are taken from BOTH body and query for POST/PUT/PATCH by standard library implementation // See: https://golang.org/pkg/net/http/#Request.ParseForm name: "ok, FORM POST bind to struct with: path + query + empty field in body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMEApplicationForm, givenContent: strings.NewReader(`id=1`), expect: &Node{ID: 1, Node: "xxx"}, }, { // NB: form values are taken from query by standard library implementation // See: https://golang.org/pkg/net/http/#Request.ParseForm name: "ok, FORM GET bind to struct with: path + query + empty field in body", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodGet, givenContentType: MIMEApplicationForm, givenContent: strings.NewReader(`id=1`), expect: &Node{ID: 0, Node: "xxx"}, // 'xxx' is taken from URL and body is not used with GET by implementation }, { name: "nok, unsupported content type", givenURL: "/api/real_node/endpoint?node=xxx", givenMethod: http.MethodPost, givenContentType: MIMETextPlain, givenContent: strings.NewReader(``), expect: &Node{ID: 0, Node: ""}, expectError: "code=415, message=Unsupported Media Type", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() // assume route we are testing is "/api/:node/endpoint?some_query_params=here" req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent) switch tc.givenContentType { case MIMEApplicationXML: req.Header.Set(HeaderContentType, MIMEApplicationXML) case MIMEApplicationForm: req.Header.Set(HeaderContentType, MIMEApplicationForm) case MIMEApplicationJSON: req.Header.Set(HeaderContentType, MIMEApplicationJSON) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) if !tc.whenNoPathParams { c.SetParamNames("node") c.SetParamValues("real_node") } var bindTarget interface{} if tc.whenBindTarget != nil { bindTarget = tc.whenBindTarget } else { bindTarget = &Node{} } b := new(DefaultBinder) err := b.BindBody(c, bindTarget) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.Equal(t, tc.expect, bindTarget) }) } } golang-github-labstack-echo-4.11.1/binder.go000066400000000000000000001255101445647333300206350ustar00rootroot00000000000000package echo import ( "encoding" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" ) /** Following functions provide handful of methods for binding to Go native types from request query or path parameters. * QueryParamsBinder(c) - binds query parameters (source URL) * PathParamsBinder(c) - binds path parameters (source URL) * FormFieldBinder(c) - binds form fields (source URL + body) Example: ```go var length int64 err := echo.QueryParamsBinder(c).Int64("length", &length).BindError() ``` For every supported type there are following methods: * ("param", &destination) - if parameter value exists then binds it to given destination of that type i.e Int64(...). * Must("param", &destination) - parameter value is required to exist, binds it to given destination of that type i.e MustInt64(...). * s("param", &destination) - (for slices) if parameter values exists then binds it to given destination of that type i.e Int64s(...). * Musts("param", &destination) - (for slices) parameter value is required to exist, binds it to given destination of that type i.e MustInt64s(...). for some slice types `BindWithDelimiter("param", &dest, ",")` supports splitting parameter values before type conversion is done i.e. URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}` `FailFast` flags binder to stop binding after first bind error during binder call chain. Enabled by default. `BindError()` returns first bind error from binder and resets errors in binder. Useful along with `FailFast()` method to do binding and returns on first problem `BindErrors()` returns all bind errors from binder and resets errors in binder. Types that are supported: * bool * float32 * float64 * int * int8 * int16 * int32 * int64 * uint * uint8/byte (does not support `bytes()`. Use BindUnmarshaler/CustomFunc to convert value from base64 etc to []byte{}) * uint16 * uint32 * uint64 * string * time * duration * BindUnmarshaler() interface * TextUnmarshaler() interface * JSONUnmarshaler() interface * UnixTime() - converts unix time (integer) to time.Time * UnixTimeMilli() - converts unix time with millisecond precision (integer) to time.Time * UnixTimeNano() - converts unix time with nanosecond precision (integer) to time.Time * CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error` */ // BindingError represents an error that occurred while binding request data. type BindingError struct { // Field is the field name where value binding failed Field string `json:"field"` // Values of parameter that failed to bind. Values []string `json:"-"` *HTTPError } // NewBindingError creates new instance of binding error func NewBindingError(sourceParam string, values []string, message interface{}, internalError error) error { return &BindingError{ Field: sourceParam, Values: values, HTTPError: &HTTPError{ Code: http.StatusBadRequest, Message: message, Internal: internalError, }, } } // Error returns error message func (be *BindingError) Error() string { return fmt.Sprintf("%s, field=%s", be.HTTPError.Error(), be.Field) } // ValueBinder provides utility methods for binding query or path parameter to various Go built-in types type ValueBinder struct { // failFast is flag for binding methods to return without attempting to bind when previous binding already failed failFast bool errors []error // ValueFunc is used to get single parameter (first) value from request ValueFunc func(sourceParam string) string // ValuesFunc is used to get all values for parameter from request. i.e. `/api/search?ids=1&ids=2` ValuesFunc func(sourceParam string) []string // ErrorFunc is used to create errors. Allows you to use your own error type, that for example marshals to your specific json response ErrorFunc func(sourceParam string, values []string, message interface{}, internalError error) error } // QueryParamsBinder creates query parameter value binder func QueryParamsBinder(c Context) *ValueBinder { return &ValueBinder{ failFast: true, ValueFunc: c.QueryParam, ValuesFunc: func(sourceParam string) []string { values, ok := c.QueryParams()[sourceParam] if !ok { return nil } return values }, ErrorFunc: NewBindingError, } } // PathParamsBinder creates path parameter value binder func PathParamsBinder(c Context) *ValueBinder { return &ValueBinder{ failFast: true, ValueFunc: c.Param, ValuesFunc: func(sourceParam string) []string { // path parameter should not have multiple values so getting values does not make sense but lets not error out here value := c.Param(sourceParam) if value == "" { return nil } return []string{value} }, ErrorFunc: NewBindingError, } } // FormFieldBinder creates form field value binder // For all requests, FormFieldBinder parses the raw query from the URL and uses query params as form fields // // For POST, PUT, and PATCH requests, it also reads the request body, parses it // as a form and uses query params as form fields. Request body parameters take precedence over URL query // string values in r.Form. // // NB: when binding forms take note that this implementation uses standard library form parsing // which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm // See https://golang.org/pkg/net/http/#Request.ParseForm func FormFieldBinder(c Context) *ValueBinder { vb := &ValueBinder{ failFast: true, ValueFunc: func(sourceParam string) string { return c.Request().FormValue(sourceParam) }, ErrorFunc: NewBindingError, } vb.ValuesFunc = func(sourceParam string) []string { if c.Request().Form == nil { // this is same as `Request().FormValue()` does internally _ = c.Request().ParseMultipartForm(32 << 20) } values, ok := c.Request().Form[sourceParam] if !ok { return nil } return values } return vb } // FailFast set internal flag to indicate if binding methods will return early (without binding) when previous bind failed // NB: call this method before any other binding methods as it modifies binding methods behaviour func (b *ValueBinder) FailFast(value bool) *ValueBinder { b.failFast = value return b } func (b *ValueBinder) setError(err error) { if b.errors == nil { b.errors = []error{err} return } b.errors = append(b.errors, err) } // BindError returns first seen bind error and resets/empties binder errors for further calls func (b *ValueBinder) BindError() error { if b.errors == nil { return nil } err := b.errors[0] b.errors = nil // reset errors so next chain will start from zero return err } // BindErrors returns all bind errors and resets/empties binder errors for further calls func (b *ValueBinder) BindErrors() []error { if b.errors == nil { return nil } errors := b.errors b.errors = nil // reset errors so next chain will start from zero return errors } // CustomFunc binds parameter values with Func. Func is called only when parameter values exist. func (b *ValueBinder) CustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder { return b.customFunc(sourceParam, customFunc, false) } // MustCustomFunc requires parameter values to exist to bind with Func. Returns error when value does not exist. func (b *ValueBinder) MustCustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder { return b.customFunc(sourceParam, customFunc, true) } func (b *ValueBinder) customFunc(sourceParam string, customFunc func(values []string) []error, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } if errs := customFunc(values); errs != nil { b.errors = append(b.errors, errs...) } return b } // String binds parameter to string variable func (b *ValueBinder) String(sourceParam string, dest *string) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { return b } *dest = value return b } // MustString requires parameter value to exist to bind to string variable. Returns error when value does not exist func (b *ValueBinder) MustString(sourceParam string, dest *string) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) return b } *dest = value return b } // Strings binds parameter values to slice of string func (b *ValueBinder) Strings(sourceParam string, dest *[]string) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValuesFunc(sourceParam) if value == nil { return b } *dest = value return b } // MustStrings requires parameter values to exist to bind to slice of string variables. Returns error when value does not exist func (b *ValueBinder) MustStrings(sourceParam string, dest *[]string) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValuesFunc(sourceParam) if value == nil { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) return b } *dest = value return b } // BindUnmarshaler binds parameter to destination implementing BindUnmarshaler interface func (b *ValueBinder) BindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } tmp := b.ValueFunc(sourceParam) if tmp == "" { return b } if err := dest.UnmarshalParam(tmp); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to BindUnmarshaler interface", err)) } return b } // MustBindUnmarshaler requires parameter value to exist to bind to destination implementing BindUnmarshaler interface. // Returns error when value does not exist func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) return b } if err := dest.UnmarshalParam(value); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to BindUnmarshaler interface", err)) } return b } // JSONUnmarshaler binds parameter to destination implementing json.Unmarshaler interface func (b *ValueBinder) JSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } tmp := b.ValueFunc(sourceParam) if tmp == "" { return b } if err := dest.UnmarshalJSON([]byte(tmp)); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err)) } return b } // MustJSONUnmarshaler requires parameter value to exist to bind to destination implementing json.Unmarshaler interface. // Returns error when value does not exist func (b *ValueBinder) MustJSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } tmp := b.ValueFunc(sourceParam) if tmp == "" { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil)) return b } if err := dest.UnmarshalJSON([]byte(tmp)); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err)) } return b } // TextUnmarshaler binds parameter to destination implementing encoding.TextUnmarshaler interface func (b *ValueBinder) TextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } tmp := b.ValueFunc(sourceParam) if tmp == "" { return b } if err := dest.UnmarshalText([]byte(tmp)); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err)) } return b } // MustTextUnmarshaler requires parameter value to exist to bind to destination implementing encoding.TextUnmarshaler interface. // Returns error when value does not exist func (b *ValueBinder) MustTextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder { if b.failFast && b.errors != nil { return b } tmp := b.ValueFunc(sourceParam) if tmp == "" { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil)) return b } if err := dest.UnmarshalText([]byte(tmp)); err != nil { b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err)) } return b } // BindWithDelimiter binds parameter to destination by suitable conversion function. // Delimiter is used before conversion to split parameter value to separate values func (b *ValueBinder) BindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder { return b.bindWithDelimiter(sourceParam, dest, delimiter, false) } // MustBindWithDelimiter requires parameter value to exist to bind destination by suitable conversion function. // Delimiter is used before conversion to split parameter value to separate values func (b *ValueBinder) MustBindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder { return b.bindWithDelimiter(sourceParam, dest, delimiter, true) } func (b *ValueBinder) bindWithDelimiter(sourceParam string, dest interface{}, delimiter string, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } tmpValues := make([]string, 0, len(values)) for _, v := range values { tmpValues = append(tmpValues, strings.Split(v, delimiter)...) } switch d := dest.(type) { case *[]string: *d = tmpValues return b case *[]bool: return b.bools(sourceParam, tmpValues, d) case *[]int64, *[]int32, *[]int16, *[]int8, *[]int: return b.ints(sourceParam, tmpValues, d) case *[]uint64, *[]uint32, *[]uint16, *[]uint8, *[]uint: // *[]byte is same as *[]uint8 return b.uints(sourceParam, tmpValues, d) case *[]float64, *[]float32: return b.floats(sourceParam, tmpValues, d) case *[]time.Duration: return b.durations(sourceParam, tmpValues, d) default: // support only cases when destination is slice // does not support time.Time as it needs argument (layout) for parsing or BindUnmarshaler b.setError(b.ErrorFunc(sourceParam, []string{}, "unsupported bind type", nil)) return b } } // Int64 binds parameter to int64 variable func (b *ValueBinder) Int64(sourceParam string, dest *int64) *ValueBinder { return b.intValue(sourceParam, dest, 64, false) } // MustInt64 requires parameter value to exist to bind to int64 variable. Returns error when value does not exist func (b *ValueBinder) MustInt64(sourceParam string, dest *int64) *ValueBinder { return b.intValue(sourceParam, dest, 64, true) } // Int32 binds parameter to int32 variable func (b *ValueBinder) Int32(sourceParam string, dest *int32) *ValueBinder { return b.intValue(sourceParam, dest, 32, false) } // MustInt32 requires parameter value to exist to bind to int32 variable. Returns error when value does not exist func (b *ValueBinder) MustInt32(sourceParam string, dest *int32) *ValueBinder { return b.intValue(sourceParam, dest, 32, true) } // Int16 binds parameter to int16 variable func (b *ValueBinder) Int16(sourceParam string, dest *int16) *ValueBinder { return b.intValue(sourceParam, dest, 16, false) } // MustInt16 requires parameter value to exist to bind to int16 variable. Returns error when value does not exist func (b *ValueBinder) MustInt16(sourceParam string, dest *int16) *ValueBinder { return b.intValue(sourceParam, dest, 16, true) } // Int8 binds parameter to int8 variable func (b *ValueBinder) Int8(sourceParam string, dest *int8) *ValueBinder { return b.intValue(sourceParam, dest, 8, false) } // MustInt8 requires parameter value to exist to bind to int8 variable. Returns error when value does not exist func (b *ValueBinder) MustInt8(sourceParam string, dest *int8) *ValueBinder { return b.intValue(sourceParam, dest, 8, true) } // Int binds parameter to int variable func (b *ValueBinder) Int(sourceParam string, dest *int) *ValueBinder { return b.intValue(sourceParam, dest, 0, false) } // MustInt requires parameter value to exist to bind to int variable. Returns error when value does not exist func (b *ValueBinder) MustInt(sourceParam string, dest *int) *ValueBinder { return b.intValue(sourceParam, dest, 0, true) } func (b *ValueBinder) intValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.int(sourceParam, value, dest, bitSize) } func (b *ValueBinder) int(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { n, err := strconv.ParseInt(value, 10, bitSize) if err != nil { if bitSize == 0 { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to int", err)) } else { b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to int%v", bitSize), err)) } return b } switch d := dest.(type) { case *int64: *d = n case *int32: *d = int32(n) case *int16: *d = int16(n) case *int8: *d = int8(n) case *int: *d = int(n) } return b } func (b *ValueBinder) intsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil)) } return b } return b.ints(sourceParam, values, dest) } func (b *ValueBinder) ints(sourceParam string, values []string, dest interface{}) *ValueBinder { switch d := dest.(type) { case *[]int64: tmp := make([]int64, len(values)) for i, v := range values { b.int(sourceParam, v, &tmp[i], 64) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]int32: tmp := make([]int32, len(values)) for i, v := range values { b.int(sourceParam, v, &tmp[i], 32) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]int16: tmp := make([]int16, len(values)) for i, v := range values { b.int(sourceParam, v, &tmp[i], 16) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]int8: tmp := make([]int8, len(values)) for i, v := range values { b.int(sourceParam, v, &tmp[i], 8) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]int: tmp := make([]int, len(values)) for i, v := range values { b.int(sourceParam, v, &tmp[i], 0) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } } return b } // Int64s binds parameter to slice of int64 func (b *ValueBinder) Int64s(sourceParam string, dest *[]int64) *ValueBinder { return b.intsValue(sourceParam, dest, false) } // MustInt64s requires parameter value to exist to bind to int64 slice variable. Returns error when value does not exist func (b *ValueBinder) MustInt64s(sourceParam string, dest *[]int64) *ValueBinder { return b.intsValue(sourceParam, dest, true) } // Int32s binds parameter to slice of int32 func (b *ValueBinder) Int32s(sourceParam string, dest *[]int32) *ValueBinder { return b.intsValue(sourceParam, dest, false) } // MustInt32s requires parameter value to exist to bind to int32 slice variable. Returns error when value does not exist func (b *ValueBinder) MustInt32s(sourceParam string, dest *[]int32) *ValueBinder { return b.intsValue(sourceParam, dest, true) } // Int16s binds parameter to slice of int16 func (b *ValueBinder) Int16s(sourceParam string, dest *[]int16) *ValueBinder { return b.intsValue(sourceParam, dest, false) } // MustInt16s requires parameter value to exist to bind to int16 slice variable. Returns error when value does not exist func (b *ValueBinder) MustInt16s(sourceParam string, dest *[]int16) *ValueBinder { return b.intsValue(sourceParam, dest, true) } // Int8s binds parameter to slice of int8 func (b *ValueBinder) Int8s(sourceParam string, dest *[]int8) *ValueBinder { return b.intsValue(sourceParam, dest, false) } // MustInt8s requires parameter value to exist to bind to int8 slice variable. Returns error when value does not exist func (b *ValueBinder) MustInt8s(sourceParam string, dest *[]int8) *ValueBinder { return b.intsValue(sourceParam, dest, true) } // Ints binds parameter to slice of int func (b *ValueBinder) Ints(sourceParam string, dest *[]int) *ValueBinder { return b.intsValue(sourceParam, dest, false) } // MustInts requires parameter value to exist to bind to int slice variable. Returns error when value does not exist func (b *ValueBinder) MustInts(sourceParam string, dest *[]int) *ValueBinder { return b.intsValue(sourceParam, dest, true) } // Uint64 binds parameter to uint64 variable func (b *ValueBinder) Uint64(sourceParam string, dest *uint64) *ValueBinder { return b.uintValue(sourceParam, dest, 64, false) } // MustUint64 requires parameter value to exist to bind to uint64 variable. Returns error when value does not exist func (b *ValueBinder) MustUint64(sourceParam string, dest *uint64) *ValueBinder { return b.uintValue(sourceParam, dest, 64, true) } // Uint32 binds parameter to uint32 variable func (b *ValueBinder) Uint32(sourceParam string, dest *uint32) *ValueBinder { return b.uintValue(sourceParam, dest, 32, false) } // MustUint32 requires parameter value to exist to bind to uint32 variable. Returns error when value does not exist func (b *ValueBinder) MustUint32(sourceParam string, dest *uint32) *ValueBinder { return b.uintValue(sourceParam, dest, 32, true) } // Uint16 binds parameter to uint16 variable func (b *ValueBinder) Uint16(sourceParam string, dest *uint16) *ValueBinder { return b.uintValue(sourceParam, dest, 16, false) } // MustUint16 requires parameter value to exist to bind to uint16 variable. Returns error when value does not exist func (b *ValueBinder) MustUint16(sourceParam string, dest *uint16) *ValueBinder { return b.uintValue(sourceParam, dest, 16, true) } // Uint8 binds parameter to uint8 variable func (b *ValueBinder) Uint8(sourceParam string, dest *uint8) *ValueBinder { return b.uintValue(sourceParam, dest, 8, false) } // MustUint8 requires parameter value to exist to bind to uint8 variable. Returns error when value does not exist func (b *ValueBinder) MustUint8(sourceParam string, dest *uint8) *ValueBinder { return b.uintValue(sourceParam, dest, 8, true) } // Byte binds parameter to byte variable func (b *ValueBinder) Byte(sourceParam string, dest *byte) *ValueBinder { return b.uintValue(sourceParam, dest, 8, false) } // MustByte requires parameter value to exist to bind to byte variable. Returns error when value does not exist func (b *ValueBinder) MustByte(sourceParam string, dest *byte) *ValueBinder { return b.uintValue(sourceParam, dest, 8, true) } // Uint binds parameter to uint variable func (b *ValueBinder) Uint(sourceParam string, dest *uint) *ValueBinder { return b.uintValue(sourceParam, dest, 0, false) } // MustUint requires parameter value to exist to bind to uint variable. Returns error when value does not exist func (b *ValueBinder) MustUint(sourceParam string, dest *uint) *ValueBinder { return b.uintValue(sourceParam, dest, 0, true) } func (b *ValueBinder) uintValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.uint(sourceParam, value, dest, bitSize) } func (b *ValueBinder) uint(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { n, err := strconv.ParseUint(value, 10, bitSize) if err != nil { if bitSize == 0 { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to uint", err)) } else { b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to uint%v", bitSize), err)) } return b } switch d := dest.(type) { case *uint64: *d = n case *uint32: *d = uint32(n) case *uint16: *d = uint16(n) case *uint8: // byte is alias to uint8 *d = uint8(n) case *uint: *d = uint(n) } return b } func (b *ValueBinder) uintsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil)) } return b } return b.uints(sourceParam, values, dest) } func (b *ValueBinder) uints(sourceParam string, values []string, dest interface{}) *ValueBinder { switch d := dest.(type) { case *[]uint64: tmp := make([]uint64, len(values)) for i, v := range values { b.uint(sourceParam, v, &tmp[i], 64) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]uint32: tmp := make([]uint32, len(values)) for i, v := range values { b.uint(sourceParam, v, &tmp[i], 32) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]uint16: tmp := make([]uint16, len(values)) for i, v := range values { b.uint(sourceParam, v, &tmp[i], 16) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]uint8: // byte is alias to uint8 tmp := make([]uint8, len(values)) for i, v := range values { b.uint(sourceParam, v, &tmp[i], 8) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]uint: tmp := make([]uint, len(values)) for i, v := range values { b.uint(sourceParam, v, &tmp[i], 0) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } } return b } // Uint64s binds parameter to slice of uint64 func (b *ValueBinder) Uint64s(sourceParam string, dest *[]uint64) *ValueBinder { return b.uintsValue(sourceParam, dest, false) } // MustUint64s requires parameter value to exist to bind to uint64 slice variable. Returns error when value does not exist func (b *ValueBinder) MustUint64s(sourceParam string, dest *[]uint64) *ValueBinder { return b.uintsValue(sourceParam, dest, true) } // Uint32s binds parameter to slice of uint32 func (b *ValueBinder) Uint32s(sourceParam string, dest *[]uint32) *ValueBinder { return b.uintsValue(sourceParam, dest, false) } // MustUint32s requires parameter value to exist to bind to uint32 slice variable. Returns error when value does not exist func (b *ValueBinder) MustUint32s(sourceParam string, dest *[]uint32) *ValueBinder { return b.uintsValue(sourceParam, dest, true) } // Uint16s binds parameter to slice of uint16 func (b *ValueBinder) Uint16s(sourceParam string, dest *[]uint16) *ValueBinder { return b.uintsValue(sourceParam, dest, false) } // MustUint16s requires parameter value to exist to bind to uint16 slice variable. Returns error when value does not exist func (b *ValueBinder) MustUint16s(sourceParam string, dest *[]uint16) *ValueBinder { return b.uintsValue(sourceParam, dest, true) } // Uint8s binds parameter to slice of uint8 func (b *ValueBinder) Uint8s(sourceParam string, dest *[]uint8) *ValueBinder { return b.uintsValue(sourceParam, dest, false) } // MustUint8s requires parameter value to exist to bind to uint8 slice variable. Returns error when value does not exist func (b *ValueBinder) MustUint8s(sourceParam string, dest *[]uint8) *ValueBinder { return b.uintsValue(sourceParam, dest, true) } // Uints binds parameter to slice of uint func (b *ValueBinder) Uints(sourceParam string, dest *[]uint) *ValueBinder { return b.uintsValue(sourceParam, dest, false) } // MustUints requires parameter value to exist to bind to uint slice variable. Returns error when value does not exist func (b *ValueBinder) MustUints(sourceParam string, dest *[]uint) *ValueBinder { return b.uintsValue(sourceParam, dest, true) } // Bool binds parameter to bool variable func (b *ValueBinder) Bool(sourceParam string, dest *bool) *ValueBinder { return b.boolValue(sourceParam, dest, false) } // MustBool requires parameter value to exist to bind to bool variable. Returns error when value does not exist func (b *ValueBinder) MustBool(sourceParam string, dest *bool) *ValueBinder { return b.boolValue(sourceParam, dest, true) } func (b *ValueBinder) boolValue(sourceParam string, dest *bool, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.bool(sourceParam, value, dest) } func (b *ValueBinder) bool(sourceParam string, value string, dest *bool) *ValueBinder { n, err := strconv.ParseBool(value) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to bool", err)) return b } *dest = n return b } func (b *ValueBinder) boolsValue(sourceParam string, dest *[]bool, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.bools(sourceParam, values, dest) } func (b *ValueBinder) bools(sourceParam string, values []string, dest *[]bool) *ValueBinder { tmp := make([]bool, len(values)) for i, v := range values { b.bool(sourceParam, v, &tmp[i]) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *dest = tmp } return b } // Bools binds parameter values to slice of bool variables func (b *ValueBinder) Bools(sourceParam string, dest *[]bool) *ValueBinder { return b.boolsValue(sourceParam, dest, false) } // MustBools requires parameter values to exist to bind to slice of bool variables. Returns error when values does not exist func (b *ValueBinder) MustBools(sourceParam string, dest *[]bool) *ValueBinder { return b.boolsValue(sourceParam, dest, true) } // Float64 binds parameter to float64 variable func (b *ValueBinder) Float64(sourceParam string, dest *float64) *ValueBinder { return b.floatValue(sourceParam, dest, 64, false) } // MustFloat64 requires parameter value to exist to bind to float64 variable. Returns error when value does not exist func (b *ValueBinder) MustFloat64(sourceParam string, dest *float64) *ValueBinder { return b.floatValue(sourceParam, dest, 64, true) } // Float32 binds parameter to float32 variable func (b *ValueBinder) Float32(sourceParam string, dest *float32) *ValueBinder { return b.floatValue(sourceParam, dest, 32, false) } // MustFloat32 requires parameter value to exist to bind to float32 variable. Returns error when value does not exist func (b *ValueBinder) MustFloat32(sourceParam string, dest *float32) *ValueBinder { return b.floatValue(sourceParam, dest, 32, true) } func (b *ValueBinder) floatValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.float(sourceParam, value, dest, bitSize) } func (b *ValueBinder) float(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { n, err := strconv.ParseFloat(value, bitSize) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to float%v", bitSize), err)) return b } switch d := dest.(type) { case *float64: *d = n case *float32: *d = float32(n) } return b } func (b *ValueBinder) floatsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.floats(sourceParam, values, dest) } func (b *ValueBinder) floats(sourceParam string, values []string, dest interface{}) *ValueBinder { switch d := dest.(type) { case *[]float64: tmp := make([]float64, len(values)) for i, v := range values { b.float(sourceParam, v, &tmp[i], 64) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } case *[]float32: tmp := make([]float32, len(values)) for i, v := range values { b.float(sourceParam, v, &tmp[i], 32) if b.failFast && b.errors != nil { return b } } if b.errors == nil { *d = tmp } } return b } // Float64s binds parameter values to slice of float64 variables func (b *ValueBinder) Float64s(sourceParam string, dest *[]float64) *ValueBinder { return b.floatsValue(sourceParam, dest, false) } // MustFloat64s requires parameter values to exist to bind to slice of float64 variables. Returns error when values does not exist func (b *ValueBinder) MustFloat64s(sourceParam string, dest *[]float64) *ValueBinder { return b.floatsValue(sourceParam, dest, true) } // Float32s binds parameter values to slice of float32 variables func (b *ValueBinder) Float32s(sourceParam string, dest *[]float32) *ValueBinder { return b.floatsValue(sourceParam, dest, false) } // MustFloat32s requires parameter values to exist to bind to slice of float32 variables. Returns error when values does not exist func (b *ValueBinder) MustFloat32s(sourceParam string, dest *[]float32) *ValueBinder { return b.floatsValue(sourceParam, dest, true) } // Time binds parameter to time.Time variable func (b *ValueBinder) Time(sourceParam string, dest *time.Time, layout string) *ValueBinder { return b.time(sourceParam, dest, layout, false) } // MustTime requires parameter value to exist to bind to time.Time variable. Returns error when value does not exist func (b *ValueBinder) MustTime(sourceParam string, dest *time.Time, layout string) *ValueBinder { return b.time(sourceParam, dest, layout, true) } func (b *ValueBinder) time(sourceParam string, dest *time.Time, layout string, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) } return b } t, err := time.Parse(layout, value) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err)) return b } *dest = t return b } // Times binds parameter values to slice of time.Time variables func (b *ValueBinder) Times(sourceParam string, dest *[]time.Time, layout string) *ValueBinder { return b.times(sourceParam, dest, layout, false) } // MustTimes requires parameter values to exist to bind to slice of time.Time variables. Returns error when values does not exist func (b *ValueBinder) MustTimes(sourceParam string, dest *[]time.Time, layout string) *ValueBinder { return b.times(sourceParam, dest, layout, true) } func (b *ValueBinder) times(sourceParam string, dest *[]time.Time, layout string, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } tmp := make([]time.Time, len(values)) for i, v := range values { t, err := time.Parse(layout, v) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Time", err)) if b.failFast { return b } continue } tmp[i] = t } if b.errors == nil { *dest = tmp } return b } // Duration binds parameter to time.Duration variable func (b *ValueBinder) Duration(sourceParam string, dest *time.Duration) *ValueBinder { return b.duration(sourceParam, dest, false) } // MustDuration requires parameter value to exist to bind to time.Duration variable. Returns error when value does not exist func (b *ValueBinder) MustDuration(sourceParam string, dest *time.Duration) *ValueBinder { return b.duration(sourceParam, dest, true) } func (b *ValueBinder) duration(sourceParam string, dest *time.Duration, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) } return b } t, err := time.ParseDuration(value) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Duration", err)) return b } *dest = t return b } // Durations binds parameter values to slice of time.Duration variables func (b *ValueBinder) Durations(sourceParam string, dest *[]time.Duration) *ValueBinder { return b.durationsValue(sourceParam, dest, false) } // MustDurations requires parameter values to exist to bind to slice of time.Duration variables. Returns error when values does not exist func (b *ValueBinder) MustDurations(sourceParam string, dest *[]time.Duration) *ValueBinder { return b.durationsValue(sourceParam, dest, true) } func (b *ValueBinder) durationsValue(sourceParam string, dest *[]time.Duration, valueMustExist bool) *ValueBinder { if b.failFast && b.errors != nil { return b } values := b.ValuesFunc(sourceParam) if len(values) == 0 { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) } return b } return b.durations(sourceParam, values, dest) } func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]time.Duration) *ValueBinder { tmp := make([]time.Duration, len(values)) for i, v := range values { t, err := time.ParseDuration(v) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Duration", err)) if b.failFast { return b } continue } tmp[i] = t } if b.errors == nil { *dest = tmp } return b } // UnixTime binds parameter to time.Time variable (in local Time corresponding to the given Unix time). // // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Second) } // MustUnixTime requires parameter value to exist to bind to time.Duration variable (in local time corresponding // to the given Unix time). Returns error when value does not exist. // // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Second) } // UnixTimeMilli binds parameter to time.Time variable (in local time corresponding to the given Unix time in millisecond precision). // // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Millisecond) } // MustUnixTimeMilli requires parameter value to exist to bind to time.Duration variable (in local time corresponding // to the given Unix time in millisecond precision). Returns error when value does not exist. // // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Millisecond) } // UnixTimeNano binds parameter to time.Time variable (in local time corresponding to the given Unix time in nanosecond precision). // // Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00 // Example: 1000000000 binds to 1970-01-01T00:00:01.000000000+00:00 // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Nanosecond) } // MustUnixTimeNano requires parameter value to exist to bind to time.Duration variable (in local Time corresponding // to the given Unix time value in nano second precision). Returns error when value does not exist. // // Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00 // Example: 1000000000 binds to 1970-01-01T00:00:01.000000000+00:00 // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // // Note: // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Nanosecond) } func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, precision time.Duration) *ValueBinder { if b.failFast && b.errors != nil { return b } value := b.ValueFunc(sourceParam) if value == "" { if valueMustExist { b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) } return b } n, err := strconv.ParseInt(value, 10, 64) if err != nil { b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err)) return b } switch precision { case time.Second: *dest = time.Unix(n, 0) case time.Millisecond: *dest = time.Unix(n/1e3, (n%1e3)*1e6) // TODO: time.UnixMilli(n) exists since Go1.17 switch to that when min version allows case time.Nanosecond: *dest = time.Unix(0, n) } return b } golang-github-labstack-echo-4.11.1/binder_external_test.go000066400000000000000000000073061445647333300236000ustar00rootroot00000000000000// run tests as external package to get real feel for API package echo_test import ( "encoding/base64" "fmt" "github.com/labstack/echo/v4" "log" "net/http" "net/http/httptest" ) func ExampleValueBinder_BindErrors() { // example route function that binds query params to different destinations and returns all bind errors in one go routeFunc := func(c echo.Context) error { var opts struct { Active bool IDs []int64 } length := int64(50) // default length is 50 b := echo.QueryParamsBinder(c) errs := b.Int64("length", &length). Int64s("ids", &opts.IDs). Bool("active", &opts.Active). BindErrors() // returns all errors if errs != nil { for _, err := range errs { bErr := err.(*echo.BindingError) log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) } return fmt.Errorf("%v fields failed to bind", len(errs)) } fmt.Printf("active = %v, length = %v, ids = %v", opts.Active, length, opts.IDs) return c.JSON(http.StatusOK, opts) } e := echo.New() c := e.NewContext( httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), httptest.NewRecorder(), ) _ = routeFunc(c) // Output: active = true, length = 25, ids = [1 2 3] } func ExampleValueBinder_BindError() { // example route function that binds query params to different destinations and stops binding on first bind error failFastRouteFunc := func(c echo.Context) error { var opts struct { Active bool IDs []int64 } length := int64(50) // default length is 50 // create binder that stops binding at first error b := echo.QueryParamsBinder(c) err := b.Int64("length", &length). Int64s("ids", &opts.IDs). Bool("active", &opts.Active). BindError() // returns first binding error if err != nil { bErr := err.(*echo.BindingError) return fmt.Errorf("my own custom error for field: %s values: %v", bErr.Field, bErr.Values) } fmt.Printf("active = %v, length = %v, ids = %v\n", opts.Active, length, opts.IDs) return c.JSON(http.StatusOK, opts) } e := echo.New() c := e.NewContext( httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), httptest.NewRecorder(), ) _ = failFastRouteFunc(c) // Output: active = true, length = 25, ids = [1 2 3] } func ExampleValueBinder_CustomFunc() { // example route function that binds query params using custom function closure routeFunc := func(c echo.Context) error { length := int64(50) // default length is 50 var binary []byte b := echo.QueryParamsBinder(c) errs := b.Int64("length", &length). CustomFunc("base64", func(values []string) []error { if len(values) == 0 { return nil } decoded, err := base64.URLEncoding.DecodeString(values[0]) if err != nil { // in this example we use only first param value but url could contain multiple params in reality and // therefore in theory produce multiple binding errors return []error{echo.NewBindingError("base64", values[0:1], "failed to decode base64", err)} } binary = decoded return nil }). BindErrors() // returns all errors if errs != nil { for _, err := range errs { bErr := err.(*echo.BindingError) log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) } return fmt.Errorf("%v fields failed to bind", len(errs)) } fmt.Printf("length = %v, base64 = %s", length, binary) return c.JSON(http.StatusOK, "ok") } e := echo.New() c := e.NewContext( httptest.NewRequest(http.MethodGet, "/api/endpoint?length=25&base64=SGVsbG8gV29ybGQ%3D", nil), httptest.NewRecorder(), ) _ = routeFunc(c) // Output: length = 25, base64 = Hello World } golang-github-labstack-echo-4.11.1/binder_test.go000066400000000000000000002722411445647333300217000ustar00rootroot00000000000000package echo import ( "encoding/json" "errors" "fmt" "github.com/stretchr/testify/assert" "io" "math/big" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" ) func createTestContext(URL string, body io.Reader, pathParams map[string]string) Context { e := New() req := httptest.NewRequest(http.MethodGet, URL, body) if body != nil { req.Header.Set(HeaderContentType, MIMEApplicationJSON) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) if len(pathParams) > 0 { names := make([]string, 0) values := make([]string, 0) for name, value := range pathParams { names = append(names, name) values = append(values, value) } c.SetParamNames(names...) c.SetParamValues(values...) } return c } func TestBindingError_Error(t *testing.T) { err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error")) assert.EqualError(t, err, `code=400, message=bind failed, internal=internal error, field=id`) bErr := err.(*BindingError) assert.Equal(t, 400, bErr.Code) assert.Equal(t, "bind failed", bErr.Message) assert.Equal(t, errors.New("internal error"), bErr.Internal) assert.Equal(t, "id", bErr.Field) assert.Equal(t, []string{"1", "nope"}, bErr.Values) } func TestBindingError_ErrorJSON(t *testing.T) { err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error")) resp, _ := json.Marshal(err) assert.Equal(t, `{"field":"id","message":"bind failed"}`, string(resp)) } func TestPathParamsBinder(t *testing.T) { c := createTestContext("/api/user/999", nil, map[string]string{ "id": "1", "nr": "2", "slice": "3", }) b := PathParamsBinder(c) id := int64(99) nr := int64(88) var slice = make([]int64, 0) var notExisting = make([]int64, 0) err := b.Int64("id", &id). Int64("nr", &nr). Int64s("slice", &slice). Int64s("not_existing", ¬Existing). BindError() assert.NoError(t, err) assert.Equal(t, int64(1), id) assert.Equal(t, int64(2), nr) assert.Equal(t, []int64{3}, slice) // binding params to slice does not make sense but it should not panic either assert.Equal(t, []int64{}, notExisting) // binding params to slice does not make sense but it should not panic either } func TestQueryParamsBinder_FailFast(t *testing.T) { var testCases = []struct { name string whenURL string givenFailFast bool expectError []string }{ { name: "ok, FailFast=true stops at first error", whenURL: "/api/user/999?nr=en&id=nope", givenFailFast: true, expectError: []string{ `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`, }, }, { name: "ok, FailFast=false encounters all errors", whenURL: "/api/user/999?nr=en&id=nope", givenFailFast: false, expectError: []string{ `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`, `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "en": invalid syntax, field=nr`, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, map[string]string{"id": "999"}) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) id := int64(99) nr := int64(88) errs := b.Int64("id", &id). Int64("nr", &nr). BindErrors() assert.Len(t, errs, len(tc.expectError)) for _, err := range errs { assert.Contains(t, tc.expectError, err.Error()) } }) } } func TestFormFieldBinder(t *testing.T) { e := New() body := `texta=foo&slice=5` req := httptest.NewRequest(http.MethodPost, "/api/search?id=1&nr=2&slice=3&slice=4", strings.NewReader(body)) req.Header.Set(HeaderContentLength, strconv.Itoa(len(body))) req.Header.Set(HeaderContentType, MIMEApplicationForm) rec := httptest.NewRecorder() c := e.NewContext(req, rec) b := FormFieldBinder(c) var texta string id := int64(99) nr := int64(88) var slice = make([]int64, 0) var notExisting = make([]int64, 0) err := b. Int64s("slice", &slice). Int64("id", &id). Int64("nr", &nr). String("texta", &texta). Int64s("notExisting", ¬Existing). BindError() assert.NoError(t, err) assert.Equal(t, "foo", texta) assert.Equal(t, int64(1), id) assert.Equal(t, int64(2), nr) assert.Equal(t, []int64{5, 3, 4}, slice) assert.Equal(t, []int64{}, notExisting) } func TestValueBinder_errorStopsBinding(t *testing.T) { // this test documents "feature" that binding multiple params can change destination if it was binded before // failing parameter binding c := createTestContext("/api/user/999?id=1&nr=nope", nil, nil) b := QueryParamsBinder(c) id := int64(99) // will be changed before nr binding fails nr := int64(88) // will not be changed err := b.Int64("id", &id). Int64("nr", &nr). BindError() assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=nr") assert.Equal(t, int64(1), id) assert.Equal(t, int64(88), nr) } func TestValueBinder_BindError(t *testing.T) { c := createTestContext("/api/user/999?nr=en&id=nope", nil, nil) b := QueryParamsBinder(c) id := int64(99) nr := int64(88) err := b.Int64("id", &id). Int64("nr", &nr). BindError() assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=id") assert.Nil(t, b.errors) assert.Nil(t, b.BindError()) } func TestValueBinder_GetValues(t *testing.T) { var testCases = []struct { name string whenValuesFunc func(sourceParam string) []string expect []int64 expectError string }{ { name: "ok, default implementation", expect: []int64{1, 101}, }, { name: "ok, values returns nil", whenValuesFunc: func(sourceParam string) []string { return nil }, expect: []int64(nil), }, { name: "ok, values returns empty slice", whenValuesFunc: func(sourceParam string) []string { return []string{} }, expect: []int64(nil), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext("/search?nr=en&id=1&id=101", nil, nil) b := QueryParamsBinder(c) if tc.whenValuesFunc != nil { b.ValuesFunc = tc.whenValuesFunc } var IDs []int64 err := b.Int64s("id", &IDs).BindError() assert.Equal(t, tc.expect, IDs) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_CustomFuncWithError(t *testing.T) { c := createTestContext("/search?nr=en&id=1&id=101", nil, nil) b := QueryParamsBinder(c) id := int64(99) givenCustomFunc := func(values []string) []error { assert.Equal(t, []string{"1", "101"}, values) return []error{ errors.New("first error"), errors.New("second error"), } } err := b.CustomFunc("id", givenCustomFunc).BindError() assert.Equal(t, int64(99), id) assert.EqualError(t, err, "first error") } func TestValueBinder_CustomFunc(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenFuncErrors []error whenURL string expectParamValues []string expectValue interface{} expectErrors []string }{ { name: "ok, binds value", whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(1000), }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nr=en", expectParamValues: []string{}, expectValue: int64(99), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(99), expectErrors: []string{"previous error"}, }, { name: "nok, func returns errors", givenFuncErrors: []error{ errors.New("first error"), errors.New("second error"), }, whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(99), expectErrors: []string{"first error", "second error"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } id := int64(99) givenCustomFunc := func(values []string) []error { assert.Equal(t, tc.expectParamValues, values) if tc.givenFuncErrors == nil { id = 1000 // emulated conversion and setting value return nil } return tc.givenFuncErrors } errs := b.CustomFunc("id", givenCustomFunc).BindErrors() assert.Equal(t, tc.expectValue, id) if tc.expectErrors != nil { assert.Len(t, errs, len(tc.expectErrors)) for _, err := range errs { assert.Contains(t, tc.expectErrors, err.Error()) } } else { assert.Nil(t, errs) } }) } } func TestValueBinder_MustCustomFunc(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenFuncErrors []error whenURL string expectParamValues []string expectValue interface{} expectErrors []string }{ { name: "ok, binds value", whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(1000), }, { name: "nok, params values empty, returns error, value is not changed", whenURL: "/search?nr=en", expectParamValues: []string{}, expectValue: int64(99), expectErrors: []string{"code=400, message=required field value is empty, field=id"}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(99), expectErrors: []string{"previous error"}, }, { name: "nok, func returns errors", givenFuncErrors: []error{ errors.New("first error"), errors.New("second error"), }, whenURL: "/search?nr=en&id=1&id=100", expectParamValues: []string{"1", "100"}, expectValue: int64(99), expectErrors: []string{"first error", "second error"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } id := int64(99) givenCustomFunc := func(values []string) []error { assert.Equal(t, tc.expectParamValues, values) if tc.givenFuncErrors == nil { id = 1000 // emulated conversion and setting value return nil } return tc.givenFuncErrors } errs := b.MustCustomFunc("id", givenCustomFunc).BindErrors() assert.Equal(t, tc.expectValue, id) if tc.expectErrors != nil { assert.Len(t, errs, len(tc.expectErrors)) for _, err := range errs { assert.Contains(t, tc.expectErrors, err.Error()) } } else { assert.Nil(t, errs) } }) } } func TestValueBinder_String(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue string expectError string }{ { name: "ok, binds value", whenURL: "/search?param=en¶m=de", expectValue: "en", }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nr=en", expectValue: "default", }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?nr=en&id=1&id=100", expectValue: "default", expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=en¶m=de", expectValue: "en", }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nr=en", expectValue: "default", expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?nr=en&id=1&id=100", expectValue: "default", expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := "default" var err error if tc.whenMust { err = b.MustString("param", &dest).BindError() } else { err = b.String("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Strings(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []string expectError string }{ { name: "ok, binds value", whenURL: "/search?param=en¶m=de", expectValue: []string{"en", "de"}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nr=en", expectValue: []string{"default"}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?nr=en&id=1&id=100", expectValue: []string{"default"}, expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=en¶m=de", expectValue: []string{"en", "de"}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nr=en", expectValue: []string{"default"}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?nr=en&id=1&id=100", expectValue: []string{"default"}, expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := []string{"default"} var err error if tc.whenMust { err = b.MustStrings("param", &dest).BindError() } else { err = b.Strings("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Int64_intValue(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue int64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=1¶m=100", expectValue: 1, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: 99, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: 99, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: 99, expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 1, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: 99, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 99, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: 99, expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := int64(99) var err error if tc.whenMust { err = b.MustInt64("param", &dest).BindError() } else { err = b.Int64("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Int_errorMessage(t *testing.T) { // int/uint (without byte size) has a little bit different error message so test these separately c := createTestContext("/search?param=nope", nil, nil) b := QueryParamsBinder(c).FailFast(false) destInt := 99 destUint := uint(98) errs := b.Int("param", &destInt).Uint("param", &destUint).BindErrors() assert.Equal(t, 99, destInt) assert.Equal(t, uint(98), destUint) assert.EqualError(t, errs[0], `code=400, message=failed to bind field value to int, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=param`) assert.EqualError(t, errs[1], `code=400, message=failed to bind field value to uint, internal=strconv.ParseUint: parsing "nope": invalid syntax, field=param`) } func TestValueBinder_Uint64_uintValue(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue uint64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=1¶m=100", expectValue: 1, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: 99, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: 99, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: 99, expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 1, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: 99, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 99, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: 99, expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := uint64(99) var err error if tc.whenMust { err = b.MustUint64("param", &dest).BindError() } else { err = b.Uint64("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Int_Types(t *testing.T) { type target struct { int64 int64 mustInt64 int64 uint64 uint64 mustUint64 uint64 int32 int32 mustInt32 int32 uint32 uint32 mustUint32 uint32 int16 int16 mustInt16 int16 uint16 uint16 mustUint16 uint16 int8 int8 mustInt8 int8 uint8 uint8 mustUint8 uint8 byte byte mustByte byte int int mustInt int uint uint mustUint uint } types := []string{ "int64=1", "mustInt64=2", "uint64=3", "mustUint64=4", "int32=5", "mustInt32=6", "uint32=7", "mustUint32=8", "int16=9", "mustInt16=10", "uint16=11", "mustUint16=12", "int8=13", "mustInt8=14", "uint8=15", "mustUint8=16", "byte=17", "mustByte=18", "int=19", "mustInt=20", "uint=21", "mustUint=22", } c := createTestContext("/search?"+strings.Join(types, "&"), nil, nil) b := QueryParamsBinder(c) dest := target{} err := b. Int64("int64", &dest.int64). MustInt64("mustInt64", &dest.mustInt64). Uint64("uint64", &dest.uint64). MustUint64("mustUint64", &dest.mustUint64). Int32("int32", &dest.int32). MustInt32("mustInt32", &dest.mustInt32). Uint32("uint32", &dest.uint32). MustUint32("mustUint32", &dest.mustUint32). Int16("int16", &dest.int16). MustInt16("mustInt16", &dest.mustInt16). Uint16("uint16", &dest.uint16). MustUint16("mustUint16", &dest.mustUint16). Int8("int8", &dest.int8). MustInt8("mustInt8", &dest.mustInt8). Uint8("uint8", &dest.uint8). MustUint8("mustUint8", &dest.mustUint8). Byte("byte", &dest.byte). MustByte("mustByte", &dest.mustByte). Int("int", &dest.int). MustInt("mustInt", &dest.mustInt). Uint("uint", &dest.uint). MustUint("mustUint", &dest.mustUint). BindError() assert.NoError(t, err) assert.Equal(t, int64(1), dest.int64) assert.Equal(t, int64(2), dest.mustInt64) assert.Equal(t, uint64(3), dest.uint64) assert.Equal(t, uint64(4), dest.mustUint64) assert.Equal(t, int32(5), dest.int32) assert.Equal(t, int32(6), dest.mustInt32) assert.Equal(t, uint32(7), dest.uint32) assert.Equal(t, uint32(8), dest.mustUint32) assert.Equal(t, int16(9), dest.int16) assert.Equal(t, int16(10), dest.mustInt16) assert.Equal(t, uint16(11), dest.uint16) assert.Equal(t, uint16(12), dest.mustUint16) assert.Equal(t, int8(13), dest.int8) assert.Equal(t, int8(14), dest.mustInt8) assert.Equal(t, uint8(15), dest.uint8) assert.Equal(t, uint8(16), dest.mustUint8) assert.Equal(t, uint8(17), dest.byte) assert.Equal(t, uint8(18), dest.mustByte) assert.Equal(t, 19, dest.int) assert.Equal(t, 20, dest.mustInt) assert.Equal(t, uint(21), dest.uint) assert.Equal(t, uint(22), dest.mustUint) } func TestValueBinder_Int64s_intsValue(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []int64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=1¶m=2¶m=1", expectValue: []int64{1, 2, 1}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []int64{99}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: []int64{99}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []int64{99}, expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1¶m=2¶m=1", expectValue: []int64{1, 2, 1}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []int64{99}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []int64{99}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []int64{99}, expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := []int64{99} // when values are set with bind - contents before bind is gone var err error if tc.whenMust { err = b.MustInt64s("param", &dest).BindError() } else { err = b.Int64s("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Uint64s_uintsValue(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []uint64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=1¶m=2¶m=1", expectValue: []uint64{1, 2, 1}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []uint64{99}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: []uint64{99}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []uint64{99}, expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1¶m=2¶m=1", expectValue: []uint64{1, 2, 1}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []uint64{99}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []uint64{99}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []uint64{99}, expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := []uint64{99} // when values are set with bind - contents before bind is gone var err error if tc.whenMust { err = b.MustUint64s("param", &dest).BindError() } else { err = b.Uint64s("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Ints_Types(t *testing.T) { type target struct { int64 []int64 mustInt64 []int64 uint64 []uint64 mustUint64 []uint64 int32 []int32 mustInt32 []int32 uint32 []uint32 mustUint32 []uint32 int16 []int16 mustInt16 []int16 uint16 []uint16 mustUint16 []uint16 int8 []int8 mustInt8 []int8 uint8 []uint8 mustUint8 []uint8 int []int mustInt []int uint []uint mustUint []uint } types := []string{ "int64=1", "mustInt64=2", "uint64=3", "mustUint64=4", "int32=5", "mustInt32=6", "uint32=7", "mustUint32=8", "int16=9", "mustInt16=10", "uint16=11", "mustUint16=12", "int8=13", "mustInt8=14", "uint8=15", "mustUint8=16", "int=19", "mustInt=20", "uint=21", "mustUint=22", } url := "/search?" for _, v := range types { url = url + "&" + v + "&" + v } c := createTestContext(url, nil, nil) b := QueryParamsBinder(c) dest := target{} err := b. Int64s("int64", &dest.int64). MustInt64s("mustInt64", &dest.mustInt64). Uint64s("uint64", &dest.uint64). MustUint64s("mustUint64", &dest.mustUint64). Int32s("int32", &dest.int32). MustInt32s("mustInt32", &dest.mustInt32). Uint32s("uint32", &dest.uint32). MustUint32s("mustUint32", &dest.mustUint32). Int16s("int16", &dest.int16). MustInt16s("mustInt16", &dest.mustInt16). Uint16s("uint16", &dest.uint16). MustUint16s("mustUint16", &dest.mustUint16). Int8s("int8", &dest.int8). MustInt8s("mustInt8", &dest.mustInt8). Uint8s("uint8", &dest.uint8). MustUint8s("mustUint8", &dest.mustUint8). Ints("int", &dest.int). MustInts("mustInt", &dest.mustInt). Uints("uint", &dest.uint). MustUints("mustUint", &dest.mustUint). BindError() assert.NoError(t, err) assert.Equal(t, []int64{1, 1}, dest.int64) assert.Equal(t, []int64{2, 2}, dest.mustInt64) assert.Equal(t, []uint64{3, 3}, dest.uint64) assert.Equal(t, []uint64{4, 4}, dest.mustUint64) assert.Equal(t, []int32{5, 5}, dest.int32) assert.Equal(t, []int32{6, 6}, dest.mustInt32) assert.Equal(t, []uint32{7, 7}, dest.uint32) assert.Equal(t, []uint32{8, 8}, dest.mustUint32) assert.Equal(t, []int16{9, 9}, dest.int16) assert.Equal(t, []int16{10, 10}, dest.mustInt16) assert.Equal(t, []uint16{11, 11}, dest.uint16) assert.Equal(t, []uint16{12, 12}, dest.mustUint16) assert.Equal(t, []int8{13, 13}, dest.int8) assert.Equal(t, []int8{14, 14}, dest.mustInt8) assert.Equal(t, []uint8{15, 15}, dest.uint8) assert.Equal(t, []uint8{16, 16}, dest.mustUint8) assert.Equal(t, []int{19, 19}, dest.int) assert.Equal(t, []int{20, 20}, dest.mustInt) assert.Equal(t, []uint{21, 21}, dest.uint) assert.Equal(t, []uint{22, 22}, dest.mustUint) } func TestValueBinder_Ints_Types_FailFast(t *testing.T) { // FailFast() should stop parsing and return early errTmpl := "code=400, message=failed to bind field value to %v, internal=strconv.Parse%v: parsing \"nope\": invalid syntax, field=param" c := createTestContext("/search?param=1¶m=nope¶m=2", nil, nil) var dest64 []int64 err := QueryParamsBinder(c).FailFast(true).Int64s("param", &dest64).BindError() assert.Equal(t, []int64(nil), dest64) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int64", "Int")) var dest32 []int32 err = QueryParamsBinder(c).FailFast(true).Int32s("param", &dest32).BindError() assert.Equal(t, []int32(nil), dest32) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int32", "Int")) var dest16 []int16 err = QueryParamsBinder(c).FailFast(true).Int16s("param", &dest16).BindError() assert.Equal(t, []int16(nil), dest16) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int16", "Int")) var dest8 []int8 err = QueryParamsBinder(c).FailFast(true).Int8s("param", &dest8).BindError() assert.Equal(t, []int8(nil), dest8) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int8", "Int")) var dest []int err = QueryParamsBinder(c).FailFast(true).Ints("param", &dest).BindError() assert.Equal(t, []int(nil), dest) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int", "Int")) var destu64 []uint64 err = QueryParamsBinder(c).FailFast(true).Uint64s("param", &destu64).BindError() assert.Equal(t, []uint64(nil), destu64) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint64", "Uint")) var destu32 []uint32 err = QueryParamsBinder(c).FailFast(true).Uint32s("param", &destu32).BindError() assert.Equal(t, []uint32(nil), destu32) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint32", "Uint")) var destu16 []uint16 err = QueryParamsBinder(c).FailFast(true).Uint16s("param", &destu16).BindError() assert.Equal(t, []uint16(nil), destu16) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint16", "Uint")) var destu8 []uint8 err = QueryParamsBinder(c).FailFast(true).Uint8s("param", &destu8).BindError() assert.Equal(t, []uint8(nil), destu8) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint8", "Uint")) var destu []uint err = QueryParamsBinder(c).FailFast(true).Uints("param", &destu).BindError() assert.Equal(t, []uint(nil), destu) assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint", "Uint")) } func TestValueBinder_Bool(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue bool expectError string }{ { name: "ok, binds value", whenURL: "/search?param=true¶m=1", expectValue: true, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: false, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: false, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: false, expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: true, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: false, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: false, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: false, expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := false var err error if tc.whenMust { err = b.MustBool("param", &dest).BindError() } else { err = b.Bool("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Bools(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []bool expectError string }{ { name: "ok, binds value", whenURL: "/search?param=true¶m=false¶m=1¶m=0", expectValue: []bool{true, false, true, false}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []bool(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenURL: "/search?param=1¶m=100", expectValue: []bool(nil), expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=true¶m=nope¶m=100", expectValue: []bool(nil), expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", }, { name: "nok, conversion fails fast, value is not changed", givenFailFast: true, whenURL: "/search?param=true¶m=nope¶m=100", expectValue: []bool(nil), expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=true¶m=false¶m=1¶m=0", expectValue: []bool{true, false, true, false}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []bool(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []bool(nil), expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []bool(nil), expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors var dest []bool var err error if tc.whenMust { err = b.MustBools("param", &dest).BindError() } else { err = b.Bools("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Float64(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue float64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=4.3¶m=1", expectValue: 4.3, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: 1.123, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: 1.123, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: 1.123, expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=4.3¶m=100", expectValue: 4.3, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: 1.123, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 1.123, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: 1.123, expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := 1.123 var err error if tc.whenMust { err = b.MustFloat64("param", &dest).BindError() } else { err = b.Float64("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Float64s(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []float64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=4.3¶m=0", expectValue: []float64{4.3, 0}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []float64(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenURL: "/search?param=1¶m=100", expectValue: []float64(nil), expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []float64(nil), expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "nok, conversion fails fast, value is not changed", givenFailFast: true, whenURL: "/search?param=0¶m=nope¶m=100", expectValue: []float64(nil), expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=4.3¶m=0", expectValue: []float64{4.3, 0}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []float64(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []float64(nil), expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []float64(nil), expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors var dest []float64 var err error if tc.whenMust { err = b.MustFloat64s("param", &dest).BindError() } else { err = b.Float64s("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Float32(t *testing.T) { var testCases = []struct { name string givenNoFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue float32 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=4.3¶m=1", expectValue: 4.3, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: 1.123, }, { name: "nok, previous errors fail fast without binding value", givenNoFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: 1.123, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: 1.123, expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=4.3¶m=100", expectValue: 4.3, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: 1.123, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenNoFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 1.123, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: 1.123, expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenNoFailFast) if tc.givenNoFailFast { b.errors = []error{errors.New("previous error")} } dest := float32(1.123) var err error if tc.whenMust { err = b.MustFloat32("param", &dest).BindError() } else { err = b.Float32("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Float32s(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []float32 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=4.3¶m=0", expectValue: []float32{4.3, 0}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []float32(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenURL: "/search?param=1¶m=100", expectValue: []float32(nil), expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []float32(nil), expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "nok, conversion fails fast, value is not changed", givenFailFast: true, whenURL: "/search?param=0¶m=nope¶m=100", expectValue: []float32(nil), expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=4.3¶m=0", expectValue: []float32{4.3, 0}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []float32(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []float32(nil), expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []float32(nil), expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors var dest []float32 var err error if tc.whenMust { err = b.MustFloat32s("param", &dest).BindError() } else { err = b.Float32s("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Time(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool whenLayout string expectValue time.Time expectError string }{ { name: "ok, binds value", whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", whenLayout: time.RFC3339, expectValue: exampleTime, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: time.Time{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", whenLayout: time.RFC3339, expectValue: exampleTime, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: time.Time{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := time.Time{} var err error if tc.whenMust { err = b.MustTime("param", &dest, tc.whenLayout).BindError() } else { err = b.Time("param", &dest, tc.whenLayout).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Times(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") exampleTime2, _ := time.Parse(time.RFC3339, "2000-01-02T09:45:31+00:00") var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool whenLayout string expectValue []time.Time expectError string }{ { name: "ok, binds value", whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", whenLayout: time.RFC3339, expectValue: []time.Time{exampleTime, exampleTime2}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []time.Time(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenURL: "/search?param=1¶m=100", expectValue: []time.Time(nil), expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", whenLayout: time.RFC3339, expectValue: []time.Time{exampleTime, exampleTime2}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []time.Time(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []time.Time(nil), expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors layout := time.RFC3339 if tc.whenLayout != "" { layout = tc.whenLayout } var dest []time.Time var err error if tc.whenMust { err = b.MustTimes("param", &dest, layout).BindError() } else { err = b.Times("param", &dest, layout).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Duration(t *testing.T) { example := 42 * time.Second var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue time.Duration expectError string }{ { name: "ok, binds value", whenURL: "/search?param=42s¶m=1ms", expectValue: example, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: 0, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: 0, expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=42s¶m=1ms", expectValue: example, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: 0, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: 0, expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest time.Duration var err error if tc.whenMust { err = b.MustDuration("param", &dest).BindError() } else { err = b.Duration("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_Durations(t *testing.T) { exampleDuration := 42 * time.Second exampleDuration2 := 1 * time.Millisecond var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []time.Duration expectError string }{ { name: "ok, binds value", whenURL: "/search?param=42s¶m=1ms", expectValue: []time.Duration{exampleDuration, exampleDuration2}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []time.Duration(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenURL: "/search?param=1¶m=100", expectValue: []time.Duration(nil), expectError: "previous error", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=42s¶m=1ms", expectValue: []time.Duration{exampleDuration, exampleDuration2}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []time.Duration(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, givenBindErrors: []error{errors.New("previous error")}, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []time.Duration(nil), expectError: "previous error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors var dest []time.Duration var err error if tc.whenMust { err = b.MustDurations("param", &dest).BindError() } else { err = b.Durations("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_BindUnmarshaler(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue Timestamp expectError string }{ { name: "ok, binds value", whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", expectValue: Timestamp(exampleTime), }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: Timestamp{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: Timestamp{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: Timestamp{}, expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", expectValue: Timestamp(exampleTime), }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: Timestamp{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: Timestamp{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: Timestamp{}, expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest Timestamp var err error if tc.whenMust { err = b.MustBindUnmarshaler("param", &dest).BindError() } else { err = b.BindUnmarshaler("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_JSONUnmarshaler(t *testing.T) { example := big.NewInt(999) var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue big.Int expectError string }{ { name: "ok, binds value", whenURL: "/search?param=999¶m=998", expectValue: *example, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: big.Int{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: big.Int{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=xxx", expectValue: big.Int{}, expectError: "code=400, message=failed to bind field value to json.Unmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=999¶m=998", expectValue: *example, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: big.Int{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=xxx", expectValue: big.Int{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=xxx", expectValue: big.Int{}, expectError: "code=400, message=failed to bind field value to json.Unmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest big.Int var err error if tc.whenMust { err = b.MustJSONUnmarshaler("param", &dest).BindError() } else { err = b.JSONUnmarshaler("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_TextUnmarshaler(t *testing.T) { example := big.NewInt(999) var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue big.Int expectError string }{ { name: "ok, binds value", whenURL: "/search?param=999¶m=998", expectValue: *example, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: big.Int{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: big.Int{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=xxx", expectValue: big.Int{}, expectError: "code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=999¶m=998", expectValue: *example, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: big.Int{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=xxx", expectValue: big.Int{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=xxx", expectValue: big.Int{}, expectError: "code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest big.Int var err error if tc.whenMust { err = b.MustTextUnmarshaler("param", &dest).BindError() } else { err = b.TextUnmarshaler("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_BindWithDelimiter_types(t *testing.T) { var testCases = []struct { name string whenURL string expect interface{} }{ { name: "ok, strings", expect: []string{"1", "2", "1"}, }, { name: "ok, int64", expect: []int64{1, 2, 1}, }, { name: "ok, int32", expect: []int32{1, 2, 1}, }, { name: "ok, int16", expect: []int16{1, 2, 1}, }, { name: "ok, int8", expect: []int8{1, 2, 1}, }, { name: "ok, int", expect: []int{1, 2, 1}, }, { name: "ok, uint64", expect: []uint64{1, 2, 1}, }, { name: "ok, uint32", expect: []uint32{1, 2, 1}, }, { name: "ok, uint16", expect: []uint16{1, 2, 1}, }, { name: "ok, uint8", expect: []uint8{1, 2, 1}, }, { name: "ok, uint", expect: []uint{1, 2, 1}, }, { name: "ok, float64", expect: []float64{1, 2, 1}, }, { name: "ok, float32", expect: []float32{1, 2, 1}, }, { name: "ok, bool", whenURL: "/search?param=1,false¶m=true", expect: []bool{true, false, true}, }, { name: "ok, Duration", whenURL: "/search?param=1s,42s¶m=1ms", expect: []time.Duration{1 * time.Second, 42 * time.Second, 1 * time.Millisecond}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { URL := "/search?param=1,2¶m=1" if tc.whenURL != "" { URL = tc.whenURL } c := createTestContext(URL, nil, nil) b := QueryParamsBinder(c) switch tc.expect.(type) { case []string: var dest []string assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []int64: var dest []int64 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []int32: var dest []int32 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []int16: var dest []int16 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []int8: var dest []int8 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []int: var dest []int assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []uint64: var dest []uint64 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []uint32: var dest []uint32 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []uint16: var dest []uint16 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []uint8: var dest []uint8 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []uint: var dest []uint assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []float64: var dest []float64 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []float32: var dest []float32 assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []bool: var dest []bool assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) case []time.Duration: var dest []time.Duration assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) assert.Equal(t, tc.expect, dest) default: assert.Fail(t, "invalid type") } }) } } func TestValueBinder_BindWithDelimiter(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []int64 expectError string }{ { name: "ok, binds value", whenURL: "/search?param=1,2¶m=1", expectValue: []int64{1, 2, 1}, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: []int64(nil), }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: []int64(nil), expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []int64(nil), expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1,2¶m=1", expectValue: []int64{1, 2, 1}, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: []int64(nil), expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: []int64(nil), expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []int64(nil), expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest []int64 var err error if tc.whenMust { err = b.MustBindWithDelimiter("param", &dest, ",").BindError() } else { err = b.BindWithDelimiter("param", &dest, ",").BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestBindWithDelimiter_invalidType(t *testing.T) { c := createTestContext("/search?param=1¶m=100", nil, nil) b := QueryParamsBinder(c) var dest []BindUnmarshaler err := b.BindWithDelimiter("param", &dest, ",").BindError() assert.Equal(t, []BindUnmarshaler(nil), dest) assert.EqualError(t, err, "code=400, message=unsupported bind type, field=param") } func TestValueBinder_UnixTime(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43+00:00") // => 1609180603 var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue time.Time expectError string }{ { name: "ok, binds value, unix time in seconds", whenURL: "/search?param=1609180603¶m=1609180604", expectValue: exampleTime, }, { name: "ok, binds value, unix time over int32 value", whenURL: "/search?param=2147483648¶m=1609180604", expectValue: time.Unix(2147483648, 0), }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: time.Time{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1609180603¶m=1609180604", expectValue: exampleTime, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: time.Time{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := time.Time{} var err error if tc.whenMust { err = b.MustUnixTime("param", &dest).BindError() } else { err = b.UnixTime("param", &dest).BindError() } assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano()) assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC)) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_UnixTimeMilli(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339Nano, "2022-03-13T15:13:30.140000000+00:00") // => 1647184410140 var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue time.Time expectError string }{ { name: "ok, binds value, unix time in milliseconds", whenURL: "/search?param=1647184410140¶m=1647184410199", expectValue: exampleTime, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: time.Time{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1647184410140¶m=1647184410199", expectValue: exampleTime, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: time.Time{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := time.Time{} var err error if tc.whenMust { err = b.MustUnixTimeMilli("param", &dest).BindError() } else { err = b.UnixTimeMilli("param", &dest).BindError() } assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano()) assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC)) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_UnixTimeNano(t *testing.T) { exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43.000000000+00:00") // => 1609180603 exampleTimeNano, _ := time.Parse(time.RFC3339Nano, "2020-12-28T18:36:43.123456789+00:00") // => 1609180603123456789 exampleTimeNanoBelowSec, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00.999999999+00:00") var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue time.Time expectError string }{ { name: "ok, binds value, unix time in nano seconds (sec precision)", whenURL: "/search?param=1609180603000000000¶m=1609180604", expectValue: exampleTime, }, { name: "ok, binds value, unix time in nano seconds", whenURL: "/search?param=1609180603123456789¶m=1609180604", expectValue: exampleTimeNano, }, { name: "ok, binds value, unix time in nano seconds (below 1 sec)", whenURL: "/search?param=999999999¶m=1609180604", expectValue: exampleTimeNanoBelowSec, }, { name: "ok, params values empty, value is not changed", whenURL: "/search?nope=1", expectValue: time.Time{}, }, { name: "nok, previous errors fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, { name: "ok (must), binds value", whenMust: true, whenURL: "/search?param=1609180603000000000¶m=1609180604", expectValue: exampleTime, }, { name: "ok (must), params values empty, returns error, value is not changed", whenMust: true, whenURL: "/search?nope=1", expectValue: time.Time{}, expectError: "code=400, message=required field value is empty, field=param", }, { name: "nok (must), previous errors fail fast without binding value", givenFailFast: true, whenMust: true, whenURL: "/search?param=1¶m=100", expectValue: time.Time{}, expectError: "previous error", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := time.Time{} var err error if tc.whenMust { err = b.MustUnixTimeNano("param", &dest).BindError() } else { err = b.UnixTimeNano("param", &dest).BindError() } assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano()) assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC)) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func BenchmarkDefaultBinder_BindInt64_single(b *testing.B) { type Opts struct { Param int64 `query:"param"` } c := createTestContext("/search?param=1¶m=100", nil, nil) b.ReportAllocs() b.ResetTimer() binder := new(DefaultBinder) for i := 0; i < b.N; i++ { var dest Opts _ = binder.Bind(&dest, c) } } func BenchmarkValueBinder_BindInt64_single(b *testing.B) { c := createTestContext("/search?param=1¶m=100", nil, nil) b.ReportAllocs() b.ResetTimer() type Opts struct { Param int64 } binder := QueryParamsBinder(c) for i := 0; i < b.N; i++ { var dest Opts _ = binder.Int64("param", &dest.Param).BindError() } } func BenchmarkRawFunc_Int64_single(b *testing.B) { c := createTestContext("/search?param=1¶m=100", nil, nil) rawFunc := func(input string, defaultValue int64) (int64, bool) { if input == "" { return defaultValue, true } n, err := strconv.Atoi(input) if err != nil { return 0, false } return int64(n), true } b.ReportAllocs() b.ResetTimer() type Opts struct { Param int64 } for i := 0; i < b.N; i++ { var dest Opts if n, ok := rawFunc(c.QueryParam("param"), 1); ok { dest.Param = n } } } func BenchmarkDefaultBinder_BindInt64_10_fields(b *testing.B) { type Opts struct { Int64 int64 `query:"int64"` Int32 int32 `query:"int32"` Int16 int16 `query:"int16"` Int8 int8 `query:"int8"` String string `query:"string"` Uint64 uint64 `query:"uint64"` Uint32 uint32 `query:"uint32"` Uint16 uint16 `query:"uint16"` Uint8 uint8 `query:"uint8"` Strings []string `query:"strings"` } c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil) b.ReportAllocs() b.ResetTimer() binder := new(DefaultBinder) for i := 0; i < b.N; i++ { var dest Opts _ = binder.Bind(&dest, c) if dest.Int64 != 1 { b.Fatalf("int64!=1") } } } func BenchmarkValueBinder_BindInt64_10_fields(b *testing.B) { type Opts struct { Int64 int64 `query:"int64"` Int32 int32 `query:"int32"` Int16 int16 `query:"int16"` Int8 int8 `query:"int8"` String string `query:"string"` Uint64 uint64 `query:"uint64"` Uint32 uint32 `query:"uint32"` Uint16 uint16 `query:"uint16"` Uint8 uint8 `query:"uint8"` Strings []string `query:"strings"` } c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil) b.ReportAllocs() b.ResetTimer() binder := QueryParamsBinder(c) for i := 0; i < b.N; i++ { var dest Opts _ = binder. Int64("int64", &dest.Int64). Int32("int32", &dest.Int32). Int16("int16", &dest.Int16). Int8("int8", &dest.Int8). String("string", &dest.String). Uint64("int64", &dest.Uint64). Uint32("int32", &dest.Uint32). Uint16("int16", &dest.Uint16). Uint8("int8", &dest.Uint8). Strings("strings", &dest.Strings). BindError() if dest.Int64 != 1 { b.Fatalf("int64!=1") } } } func TestValueBinder_TimeError(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool whenLayout string expectValue time.Time expectError string }{ { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: time.Time{}, expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } dest := time.Time{} var err error if tc.whenMust { err = b.MustTime("param", &dest, tc.whenLayout).BindError() } else { err = b.Time("param", &dest, tc.whenLayout).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_TimesError(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool whenLayout string expectValue []time.Time expectError string }{ { name: "nok, fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: []time.Time(nil), expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"1\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"1\" as \"2006\", field=param", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []time.Time(nil), expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []time.Time(nil), expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors layout := time.RFC3339 if tc.whenLayout != "" { layout = tc.whenLayout } var dest []time.Time var err error if tc.whenMust { err = b.MustTimes("param", &dest, layout).BindError() } else { err = b.Times("param", &dest, layout).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_DurationError(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue time.Duration expectError string }{ { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: 0, expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: 0, expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) if tc.givenFailFast { b.errors = []error{errors.New("previous error")} } var dest time.Duration var err error if tc.whenMust { err = b.MustDuration("param", &dest).BindError() } else { err = b.Duration("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValueBinder_DurationsError(t *testing.T) { var testCases = []struct { name string givenFailFast bool givenBindErrors []error whenURL string whenMust bool expectValue []time.Duration expectError string }{ { name: "nok, fail fast without binding value", givenFailFast: true, whenURL: "/search?param=1¶m=100", expectValue: []time.Duration(nil), expectError: "code=400, message=failed to bind field value to Duration, internal=time: missing unit in duration \"1\", field=param", }, { name: "nok, conversion fails, value is not changed", whenURL: "/search?param=nope¶m=100", expectValue: []time.Duration(nil), expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", }, { name: "nok (must), conversion fails, value is not changed", whenMust: true, whenURL: "/search?param=nope¶m=100", expectValue: []time.Duration(nil), expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { c := createTestContext(tc.whenURL, nil, nil) b := QueryParamsBinder(c).FailFast(tc.givenFailFast) b.errors = tc.givenBindErrors var dest []time.Duration var err error if tc.whenMust { err = b.MustDurations("param", &dest).BindError() } else { err = b.Durations("param", &dest).BindError() } assert.Equal(t, tc.expectValue, dest) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } golang-github-labstack-echo-4.11.1/codecov.yml000066400000000000000000000002271445647333300212050ustar00rootroot00000000000000coverage: status: project: default: threshold: 1% patch: default: threshold: 1% comment: require_changes: truegolang-github-labstack-echo-4.11.1/context.go000066400000000000000000000410621445647333300210550ustar00rootroot00000000000000package echo import ( "bytes" "encoding/xml" "fmt" "io" "mime/multipart" "net" "net/http" "net/url" "strings" "sync" ) type ( // Context represents the context of the current HTTP request. It holds request and // response objects, path, path parameters, data and registered handler. Context interface { // Request returns `*http.Request`. Request() *http.Request // SetRequest sets `*http.Request`. SetRequest(r *http.Request) // SetResponse sets `*Response`. SetResponse(r *Response) // Response returns `*Response`. Response() *Response // IsTLS returns true if HTTP connection is TLS otherwise false. IsTLS() bool // IsWebSocket returns true if HTTP connection is WebSocket otherwise false. IsWebSocket() bool // Scheme returns the HTTP protocol scheme, `http` or `https`. Scheme() string // RealIP returns the client's network address based on `X-Forwarded-For` // or `X-Real-IP` request header. // The behavior can be configured using `Echo#IPExtractor`. RealIP() string // Path returns the registered path for the handler. Path() string // SetPath sets the registered path for the handler. SetPath(p string) // Param returns path parameter by name. Param(name string) string // ParamNames returns path parameter names. ParamNames() []string // SetParamNames sets path parameter names. SetParamNames(names ...string) // ParamValues returns path parameter values. ParamValues() []string // SetParamValues sets path parameter values. SetParamValues(values ...string) // QueryParam returns the query param for the provided name. QueryParam(name string) string // QueryParams returns the query parameters as `url.Values`. QueryParams() url.Values // QueryString returns the URL query string. QueryString() string // FormValue returns the form field value for the provided name. FormValue(name string) string // FormParams returns the form parameters as `url.Values`. FormParams() (url.Values, error) // FormFile returns the multipart form file for the provided name. FormFile(name string) (*multipart.FileHeader, error) // MultipartForm returns the multipart form. MultipartForm() (*multipart.Form, error) // Cookie returns the named cookie provided in the request. Cookie(name string) (*http.Cookie, error) // SetCookie adds a `Set-Cookie` header in HTTP response. SetCookie(cookie *http.Cookie) // Cookies returns the HTTP cookies sent with the request. Cookies() []*http.Cookie // Get retrieves data from the context. Get(key string) interface{} // Set saves data in the context. Set(key string, val interface{}) // Bind binds path params, query params and the request body into provided type `i`. The default binder // binds body based on Content-Type header. Bind(i interface{}) error // Validate validates provided `i`. It is usually called after `Context#Bind()`. // Validator must be registered using `Echo#Validator`. Validate(i interface{}) error // Render renders a template with data and sends a text/html response with status // code. Renderer must be registered using `Echo.Renderer`. Render(code int, name string, data interface{}) error // HTML sends an HTTP response with status code. HTML(code int, html string) error // HTMLBlob sends an HTTP blob response with status code. HTMLBlob(code int, b []byte) error // String sends a string response with status code. String(code int, s string) error // JSON sends a JSON response with status code. JSON(code int, i interface{}) error // JSONPretty sends a pretty-print JSON with status code. JSONPretty(code int, i interface{}, indent string) error // JSONBlob sends a JSON blob response with status code. JSONBlob(code int, b []byte) error // JSONP sends a JSONP response with status code. It uses `callback` to construct // the JSONP payload. JSONP(code int, callback string, i interface{}) error // JSONPBlob sends a JSONP blob response with status code. It uses `callback` // to construct the JSONP payload. JSONPBlob(code int, callback string, b []byte) error // XML sends an XML response with status code. XML(code int, i interface{}) error // XMLPretty sends a pretty-print XML with status code. XMLPretty(code int, i interface{}, indent string) error // XMLBlob sends an XML blob response with status code. XMLBlob(code int, b []byte) error // Blob sends a blob response with status code and content type. Blob(code int, contentType string, b []byte) error // Stream sends a streaming response with status code and content type. Stream(code int, contentType string, r io.Reader) error // File sends a response with the content of the file. File(file string) error // Attachment sends a response as attachment, prompting client to save the // file. Attachment(file string, name string) error // Inline sends a response as inline, opening the file in the browser. Inline(file string, name string) error // NoContent sends a response with no body and a status code. NoContent(code int) error // Redirect redirects the request to a provided URL with status code. Redirect(code int, url string) error // Error invokes the registered global HTTP error handler. Generally used by middleware. // A side-effect of calling global error handler is that now Response has been committed (sent to the client) and // middlewares up in chain can not change Response status code or Response body anymore. // // Avoid using this method in handlers as no middleware will be able to effectively handle errors after that. Error(err error) // Handler returns the matched handler by router. Handler() HandlerFunc // SetHandler sets the matched handler by router. SetHandler(h HandlerFunc) // Logger returns the `Logger` instance. Logger() Logger // SetLogger Set the logger SetLogger(l Logger) // Echo returns the `Echo` instance. Echo() *Echo // Reset resets the context after request completes. It must be called along // with `Echo#AcquireContext()` and `Echo#ReleaseContext()`. // See `Echo#ServeHTTP()` Reset(r *http.Request, w http.ResponseWriter) } context struct { request *http.Request response *Response path string pnames []string pvalues []string query url.Values handler HandlerFunc store Map echo *Echo logger Logger lock sync.RWMutex } ) const ( // ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain. // Allow header is mandatory for status 405 (method not found) and useful for OPTIONS method requests. // It is added to context only when Router does not find matching method handler for request. ContextKeyHeaderAllow = "echo_header_allow" ) const ( defaultMemory = 32 << 20 // 32 MB indexPage = "index.html" defaultIndent = " " ) func (c *context) writeContentType(value string) { header := c.Response().Header() if header.Get(HeaderContentType) == "" { header.Set(HeaderContentType, value) } } func (c *context) Request() *http.Request { return c.request } func (c *context) SetRequest(r *http.Request) { c.request = r } func (c *context) Response() *Response { return c.response } func (c *context) SetResponse(r *Response) { c.response = r } func (c *context) IsTLS() bool { return c.request.TLS != nil } func (c *context) IsWebSocket() bool { upgrade := c.request.Header.Get(HeaderUpgrade) return strings.EqualFold(upgrade, "websocket") } func (c *context) Scheme() string { // Can't use `r.Request.URL.Scheme` // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 if c.IsTLS() { return "https" } if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" { return scheme } if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" { return scheme } if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" { return "https" } if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" { return scheme } return "http" } func (c *context) RealIP() string { if c.echo != nil && c.echo.IPExtractor != nil { return c.echo.IPExtractor(c.request) } // Fall back to legacy behavior if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { i := strings.IndexAny(ip, ",") if i > 0 { xffip := strings.TrimSpace(ip[:i]) xffip = strings.TrimPrefix(xffip, "[") xffip = strings.TrimSuffix(xffip, "]") return xffip } return ip } if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { ip = strings.TrimPrefix(ip, "[") ip = strings.TrimSuffix(ip, "]") return ip } ra, _, _ := net.SplitHostPort(c.request.RemoteAddr) return ra } func (c *context) Path() string { return c.path } func (c *context) SetPath(p string) { c.path = p } func (c *context) Param(name string) string { for i, n := range c.pnames { if i < len(c.pvalues) { if n == name { return c.pvalues[i] } } } return "" } func (c *context) ParamNames() []string { return c.pnames } func (c *context) SetParamNames(names ...string) { c.pnames = names l := len(names) if *c.echo.maxParam < l { *c.echo.maxParam = l } if len(c.pvalues) < l { // Keeping the old pvalues just for backward compatibility, but it sounds that doesn't make sense to keep them, // probably those values will be overriden in a Context#SetParamValues newPvalues := make([]string, l) copy(newPvalues, c.pvalues) c.pvalues = newPvalues } } func (c *context) ParamValues() []string { return c.pvalues[:len(c.pnames)] } func (c *context) SetParamValues(values ...string) { // NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam at all times // It will brake the Router#Find code limit := len(values) if limit > *c.echo.maxParam { limit = *c.echo.maxParam } for i := 0; i < limit; i++ { c.pvalues[i] = values[i] } } func (c *context) QueryParam(name string) string { if c.query == nil { c.query = c.request.URL.Query() } return c.query.Get(name) } func (c *context) QueryParams() url.Values { if c.query == nil { c.query = c.request.URL.Query() } return c.query } func (c *context) QueryString() string { return c.request.URL.RawQuery } func (c *context) FormValue(name string) string { return c.request.FormValue(name) } func (c *context) FormParams() (url.Values, error) { if strings.HasPrefix(c.request.Header.Get(HeaderContentType), MIMEMultipartForm) { if err := c.request.ParseMultipartForm(defaultMemory); err != nil { return nil, err } } else { if err := c.request.ParseForm(); err != nil { return nil, err } } return c.request.Form, nil } func (c *context) FormFile(name string) (*multipart.FileHeader, error) { f, fh, err := c.request.FormFile(name) if err != nil { return nil, err } f.Close() return fh, nil } func (c *context) MultipartForm() (*multipart.Form, error) { err := c.request.ParseMultipartForm(defaultMemory) return c.request.MultipartForm, err } func (c *context) Cookie(name string) (*http.Cookie, error) { return c.request.Cookie(name) } func (c *context) SetCookie(cookie *http.Cookie) { http.SetCookie(c.Response(), cookie) } func (c *context) Cookies() []*http.Cookie { return c.request.Cookies() } func (c *context) Get(key string) interface{} { c.lock.RLock() defer c.lock.RUnlock() return c.store[key] } func (c *context) Set(key string, val interface{}) { c.lock.Lock() defer c.lock.Unlock() if c.store == nil { c.store = make(Map) } c.store[key] = val } func (c *context) Bind(i interface{}) error { return c.echo.Binder.Bind(i, c) } func (c *context) Validate(i interface{}) error { if c.echo.Validator == nil { return ErrValidatorNotRegistered } return c.echo.Validator.Validate(i) } func (c *context) Render(code int, name string, data interface{}) (err error) { if c.echo.Renderer == nil { return ErrRendererNotRegistered } buf := new(bytes.Buffer) if err = c.echo.Renderer.Render(buf, name, data, c); err != nil { return } return c.HTMLBlob(code, buf.Bytes()) } func (c *context) HTML(code int, html string) (err error) { return c.HTMLBlob(code, []byte(html)) } func (c *context) HTMLBlob(code int, b []byte) (err error) { return c.Blob(code, MIMETextHTMLCharsetUTF8, b) } func (c *context) String(code int, s string) (err error) { return c.Blob(code, MIMETextPlainCharsetUTF8, []byte(s)) } func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error) { indent := "" if _, pretty := c.QueryParams()["pretty"]; c.echo.Debug || pretty { indent = defaultIndent } c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(callback + "(")); err != nil { return } if err = c.echo.JSONSerializer.Serialize(c, i, indent); err != nil { return } if _, err = c.response.Write([]byte(");")); err != nil { return } return } func (c *context) json(code int, i interface{}, indent string) error { c.writeContentType(MIMEApplicationJSONCharsetUTF8) c.response.Status = code return c.echo.JSONSerializer.Serialize(c, i, indent) } func (c *context) JSON(code int, i interface{}) (err error) { indent := "" if _, pretty := c.QueryParams()["pretty"]; c.echo.Debug || pretty { indent = defaultIndent } return c.json(code, i, indent) } func (c *context) JSONPretty(code int, i interface{}, indent string) (err error) { return c.json(code, i, indent) } func (c *context) JSONBlob(code int, b []byte) (err error) { return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b) } func (c *context) JSONP(code int, callback string, i interface{}) (err error) { return c.jsonPBlob(code, callback, i) } func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) { c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(callback + "(")); err != nil { return } if _, err = c.response.Write(b); err != nil { return } _, err = c.response.Write([]byte(");")) return } func (c *context) xml(code int, i interface{}, indent string) (err error) { c.writeContentType(MIMEApplicationXMLCharsetUTF8) c.response.WriteHeader(code) enc := xml.NewEncoder(c.response) if indent != "" { enc.Indent("", indent) } if _, err = c.response.Write([]byte(xml.Header)); err != nil { return } return enc.Encode(i) } func (c *context) XML(code int, i interface{}) (err error) { indent := "" if _, pretty := c.QueryParams()["pretty"]; c.echo.Debug || pretty { indent = defaultIndent } return c.xml(code, i, indent) } func (c *context) XMLPretty(code int, i interface{}, indent string) (err error) { return c.xml(code, i, indent) } func (c *context) XMLBlob(code int, b []byte) (err error) { c.writeContentType(MIMEApplicationXMLCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(xml.Header)); err != nil { return } _, err = c.response.Write(b) return } func (c *context) Blob(code int, contentType string, b []byte) (err error) { c.writeContentType(contentType) c.response.WriteHeader(code) _, err = c.response.Write(b) return } func (c *context) Stream(code int, contentType string, r io.Reader) (err error) { c.writeContentType(contentType) c.response.WriteHeader(code) _, err = io.Copy(c.response, r) return } func (c *context) Attachment(file, name string) error { return c.contentDisposition(file, name, "attachment") } func (c *context) Inline(file, name string) error { return c.contentDisposition(file, name, "inline") } func (c *context) contentDisposition(file, name, dispositionType string) error { c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name)) return c.File(file) } func (c *context) NoContent(code int) error { c.response.WriteHeader(code) return nil } func (c *context) Redirect(code int, url string) error { if code < 300 || code > 308 { return ErrInvalidRedirectCode } c.response.Header().Set(HeaderLocation, url) c.response.WriteHeader(code) return nil } func (c *context) Error(err error) { c.echo.HTTPErrorHandler(err, c) } func (c *context) Echo() *Echo { return c.echo } func (c *context) Handler() HandlerFunc { return c.handler } func (c *context) SetHandler(h HandlerFunc) { c.handler = h } func (c *context) Logger() Logger { res := c.logger if res != nil { return res } return c.echo.Logger } func (c *context) SetLogger(l Logger) { c.logger = l } func (c *context) Reset(r *http.Request, w http.ResponseWriter) { c.request = r c.response.reset(w) c.query = nil c.handler = NotFoundHandler c.store = nil c.path = "" c.pnames = nil c.logger = nil // NOTE: Don't reset because it has to have length c.echo.maxParam at all times for i := 0; i < *c.echo.maxParam; i++ { c.pvalues[i] = "" } } golang-github-labstack-echo-4.11.1/context_fs.go000066400000000000000000000023641445647333300215470ustar00rootroot00000000000000package echo import ( "errors" "io" "io/fs" "net/http" "path/filepath" ) func (c *context) File(file string) error { return fsFile(c, file, c.echo.Filesystem) } // FileFS serves file from given file system. // // When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary // prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths // including `assets/images` as their prefix. func (c *context) FileFS(file string, filesystem fs.FS) error { return fsFile(c, file, filesystem) } func fsFile(c Context, file string, filesystem fs.FS) error { f, err := filesystem.Open(file) if err != nil { return ErrNotFound } defer f.Close() fi, _ := f.Stat() if fi.IsDir() { file = filepath.ToSlash(filepath.Join(file, indexPage)) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect. f, err = filesystem.Open(file) if err != nil { return ErrNotFound } defer f.Close() if fi, err = f.Stat(); err != nil { return err } } ff, ok := f.(io.ReadSeeker) if !ok { return errors.New("file does not implement io.ReadSeeker") } http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff) return nil } golang-github-labstack-echo-4.11.1/context_fs_test.go000066400000000000000000000061061445647333300226040ustar00rootroot00000000000000package echo import ( "github.com/stretchr/testify/assert" "io/fs" "net/http" "net/http/httptest" "os" "testing" ) func TestContext_File(t *testing.T) { var testCases = []struct { name string whenFile string whenFS fs.FS expectStatus int expectStartsWith []byte expectError string }{ { name: "ok, from default file system", whenFile: "_fixture/images/walle.png", whenFS: nil, expectStatus: http.StatusOK, expectStartsWith: []byte{0x89, 0x50, 0x4e}, }, { name: "ok, from custom file system", whenFile: "walle.png", whenFS: os.DirFS("_fixture/images"), expectStatus: http.StatusOK, expectStartsWith: []byte{0x89, 0x50, 0x4e}, }, { name: "nok, not existent file", whenFile: "not.png", whenFS: os.DirFS("_fixture/images"), expectStatus: http.StatusOK, expectStartsWith: nil, expectError: "code=404, message=Not Found", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() if tc.whenFS != nil { e.Filesystem = tc.whenFS } handler := func(ec Context) error { return ec.(*context).File(tc.whenFile) } req := httptest.NewRequest(http.MethodGet, "/match.png", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.Equal(t, tc.expectStatus, rec.Code) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } body := rec.Body.Bytes() if len(body) > len(tc.expectStartsWith) { body = body[:len(tc.expectStartsWith)] } assert.Equal(t, tc.expectStartsWith, body) }) } } func TestContext_FileFS(t *testing.T) { var testCases = []struct { name string whenFile string whenFS fs.FS expectStatus int expectStartsWith []byte expectError string }{ { name: "ok", whenFile: "walle.png", whenFS: os.DirFS("_fixture/images"), expectStatus: http.StatusOK, expectStartsWith: []byte{0x89, 0x50, 0x4e}, }, { name: "nok, not existent file", whenFile: "not.png", whenFS: os.DirFS("_fixture/images"), expectStatus: http.StatusOK, expectStartsWith: nil, expectError: "code=404, message=Not Found", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() handler := func(ec Context) error { return ec.(*context).FileFS(tc.whenFile, tc.whenFS) } req := httptest.NewRequest(http.MethodGet, "/match.png", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.Equal(t, tc.expectStatus, rec.Code) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } body := rec.Body.Bytes() if len(body) > len(tc.expectStartsWith) { body = body[:len(tc.expectStartsWith)] } assert.Equal(t, tc.expectStartsWith, body) }) } } golang-github-labstack-echo-4.11.1/context_test.go000066400000000000000000000573301445647333300221210ustar00rootroot00000000000000package echo import ( "bytes" "crypto/tls" "encoding/json" "encoding/xml" "errors" "fmt" "io" "math" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strings" "testing" "text/template" "time" "github.com/labstack/gommon/log" testify "github.com/stretchr/testify/assert" ) type ( Template struct { templates *template.Template } ) var testUser = user{1, "Jon Snow"} func BenchmarkAllocJSONP(b *testing.B) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { c.JSONP(http.StatusOK, "callback", testUser) } } func BenchmarkAllocJSON(b *testing.B) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { c.JSON(http.StatusOK, testUser) } } func BenchmarkAllocXML(b *testing.B) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { c.XML(http.StatusOK, testUser) } } func BenchmarkRealIPForHeaderXForwardFor(b *testing.B) { c := context{request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1, 127.0.1.1, "}}, }} for i := 0; i < b.N; i++ { c.RealIP() } } func (t *Template) Render(w io.Writer, name string, data interface{}, c Context) error { return t.templates.ExecuteTemplate(w, name, data) } type responseWriterErr struct { } func (responseWriterErr) Header() http.Header { return http.Header{} } func (responseWriterErr) Write([]byte) (int, error) { return 0, errors.New("err") } func (responseWriterErr) WriteHeader(statusCode int) { } func TestContext(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) assert := testify.New(t) // Echo assert.Equal(e, c.Echo()) // Request assert.NotNil(c.Request()) // Response assert.NotNil(c.Response()) //-------- // Render //-------- tmpl := &Template{ templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")), } c.echo.Renderer = tmpl err := c.Render(http.StatusOK, "hello", "Jon Snow") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal("Hello, Jon Snow!", rec.Body.String()) } c.echo.Renderer = nil err = c.Render(http.StatusOK, "hello", "Jon Snow") assert.Error(err) // JSON rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.JSON(http.StatusOK, user{1, "Jon Snow"}) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(userJSON+"\n", rec.Body.String()) } // JSON with "?pretty" req = httptest.NewRequest(http.MethodGet, "/?pretty", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.JSON(http.StatusOK, user{1, "Jon Snow"}) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(userJSONPretty+"\n", rec.Body.String()) } req = httptest.NewRequest(http.MethodGet, "/", nil) // reset // JSONPretty rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.JSONPretty(http.StatusOK, user{1, "Jon Snow"}, " ") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(userJSONPretty+"\n", rec.Body.String()) } // JSON (error) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.JSON(http.StatusOK, make(chan bool)) assert.Error(err) // JSONP rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) callback := "callback" err = c.JSONP(http.StatusOK, callback, user{1, "Jon Snow"}) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(callback+"("+userJSON+"\n);", rec.Body.String()) } // XML rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.XML(http.StatusOK, user{1, "Jon Snow"}) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(xml.Header+userXML, rec.Body.String()) } // XML with "?pretty" req = httptest.NewRequest(http.MethodGet, "/?pretty", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.XML(http.StatusOK, user{1, "Jon Snow"}) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(xml.Header+userXMLPretty, rec.Body.String()) } req = httptest.NewRequest(http.MethodGet, "/", nil) // XML (error) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.XML(http.StatusOK, make(chan bool)) assert.Error(err) // XML response write error c = e.NewContext(req, rec).(*context) c.response.Writer = responseWriterErr{} err = c.XML(0, 0) testify.Error(t, err) // XMLPretty rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.XMLPretty(http.StatusOK, user{1, "Jon Snow"}, " ") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(xml.Header+userXMLPretty, rec.Body.String()) } t.Run("empty indent", func(t *testing.T) { var ( u = user{1, "Jon Snow"} buf = new(bytes.Buffer) emptyIndent = "" ) t.Run("json", func(t *testing.T) { buf.Reset() assert := testify.New(t) // New JSONBlob with empty indent rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) enc := json.NewEncoder(buf) enc.SetIndent(emptyIndent, emptyIndent) err = enc.Encode(u) err = c.json(http.StatusOK, user{1, "Jon Snow"}, emptyIndent) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(buf.String(), rec.Body.String()) } }) t.Run("xml", func(t *testing.T) { buf.Reset() assert := testify.New(t) // New XMLBlob with empty indent rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) enc := xml.NewEncoder(buf) enc.Indent(emptyIndent, emptyIndent) err = enc.Encode(u) err = c.xml(http.StatusOK, user{1, "Jon Snow"}, emptyIndent) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(xml.Header+buf.String(), rec.Body.String()) } }) }) // Legacy JSONBlob rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) data, err := json.Marshal(user{1, "Jon Snow"}) assert.NoError(err) err = c.JSONBlob(http.StatusOK, data) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(userJSON, rec.Body.String()) } // Legacy JSONPBlob rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) callback = "callback" data, err = json.Marshal(user{1, "Jon Snow"}) assert.NoError(err) err = c.JSONPBlob(http.StatusOK, callback, data) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(callback+"("+userJSON+");", rec.Body.String()) } // Legacy XMLBlob rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) data, err = xml.Marshal(user{1, "Jon Snow"}) assert.NoError(err) err = c.XMLBlob(http.StatusOK, data) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(xml.Header+userXML, rec.Body.String()) } // String rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.String(http.StatusOK, "Hello, World!") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMETextPlainCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal("Hello, World!", rec.Body.String()) } // HTML rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.HTML(http.StatusOK, "Hello, World!") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(MIMETextHTMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal("Hello, World!", rec.Body.String()) } // Stream rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) r := strings.NewReader("response from a stream") err = c.Stream(http.StatusOK, "application/octet-stream", r) if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal("application/octet-stream", rec.Header().Get(HeaderContentType)) assert.Equal("response from a stream", rec.Body.String()) } // Attachment rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.Attachment("_fixture/images/walle.png", "walle.png") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal("attachment; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition)) assert.Equal(219885, rec.Body.Len()) } // Inline rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = c.Inline("_fixture/images/walle.png", "walle.png") if assert.NoError(err) { assert.Equal(http.StatusOK, rec.Code) assert.Equal("inline; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition)) assert.Equal(219885, rec.Body.Len()) } // NoContent rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) c.NoContent(http.StatusOK) assert.Equal(http.StatusOK, rec.Code) // Error rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) c.Error(errors.New("error")) assert.Equal(http.StatusInternalServerError, rec.Code) // Reset c.SetParamNames("foo") c.SetParamValues("bar") c.Set("foe", "ban") c.query = url.Values(map[string][]string{"fon": {"baz"}}) c.Reset(req, httptest.NewRecorder()) assert.Equal(0, len(c.ParamValues())) assert.Equal(0, len(c.ParamNames())) assert.Equal(0, len(c.store)) assert.Equal("", c.Path()) assert.Equal(0, len(c.QueryParams())) } func TestContext_JSON_CommitsCustomResponseCode(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) err := c.JSON(http.StatusCreated, user{1, "Jon Snow"}) assert := testify.New(t) if assert.NoError(err) { assert.Equal(http.StatusCreated, rec.Code) assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(userJSON+"\n", rec.Body.String()) } } func TestContext_JSON_DoesntCommitResponseCodePrematurely(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) err := c.JSON(http.StatusCreated, map[string]float64{"a": math.NaN()}) assert := testify.New(t) if assert.Error(err) { assert.False(c.response.Committed) } } func TestContextCookie(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) theme := "theme=light" user := "user=Jon Snow" req.Header.Add(HeaderCookie, theme) req.Header.Add(HeaderCookie, user) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) assert := testify.New(t) // Read single cookie, err := c.Cookie("theme") if assert.NoError(err) { assert.Equal("theme", cookie.Name) assert.Equal("light", cookie.Value) } // Read multiple for _, cookie := range c.Cookies() { switch cookie.Name { case "theme": assert.Equal("light", cookie.Value) case "user": assert.Equal("Jon Snow", cookie.Value) } } // Write cookie = &http.Cookie{ Name: "SSID", Value: "Ap4PGTEq", Domain: "labstack.com", Path: "/", Expires: time.Now(), Secure: true, HttpOnly: true, } c.SetCookie(cookie) assert.Contains(rec.Header().Get(HeaderSetCookie), "SSID") assert.Contains(rec.Header().Get(HeaderSetCookie), "Ap4PGTEq") assert.Contains(rec.Header().Get(HeaderSetCookie), "labstack.com") assert.Contains(rec.Header().Get(HeaderSetCookie), "Secure") assert.Contains(rec.Header().Get(HeaderSetCookie), "HttpOnly") } func TestContextPath(t *testing.T) { e := New() r := e.Router() handler := func(c Context) error { return c.String(http.StatusOK, "OK") } r.Add(http.MethodGet, "/users/:id", handler) c := e.NewContext(nil, nil) r.Find(http.MethodGet, "/users/1", c) assert := testify.New(t) assert.Equal("/users/:id", c.Path()) r.Add(http.MethodGet, "/users/:uid/files/:fid", handler) c = e.NewContext(nil, nil) r.Find(http.MethodGet, "/users/1/files/1", c) assert.Equal("/users/:uid/files/:fid", c.Path()) } func TestContextPathParam(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) c := e.NewContext(req, nil) // ParamNames c.SetParamNames("uid", "fid") testify.EqualValues(t, []string{"uid", "fid"}, c.ParamNames()) // ParamValues c.SetParamValues("101", "501") testify.EqualValues(t, []string{"101", "501"}, c.ParamValues()) // Param testify.Equal(t, "501", c.Param("fid")) testify.Equal(t, "", c.Param("undefined")) } func TestContextGetAndSetParam(t *testing.T) { e := New() r := e.Router() r.Add(http.MethodGet, "/:foo", func(Context) error { return nil }) req := httptest.NewRequest(http.MethodGet, "/:foo", nil) c := e.NewContext(req, nil) c.SetParamNames("foo") // round-trip param values with modification paramVals := c.ParamValues() testify.EqualValues(t, []string{""}, c.ParamValues()) paramVals[0] = "bar" c.SetParamValues(paramVals...) testify.EqualValues(t, []string{"bar"}, c.ParamValues()) // shouldn't explode during Reset() afterwards! testify.NotPanics(t, func() { c.Reset(nil, nil) }) } // Issue #1655 func TestContextSetParamNamesShouldUpdateEchoMaxParam(t *testing.T) { assert := testify.New(t) e := New() assert.Equal(0, *e.maxParam) expectedOneParam := []string{"one"} expectedTwoParams := []string{"one", "two"} expectedThreeParams := []string{"one", "two", ""} expectedABCParams := []string{"A", "B", "C"} c := e.NewContext(nil, nil) c.SetParamNames("1", "2") c.SetParamValues(expectedTwoParams...) assert.Equal(2, *e.maxParam) assert.EqualValues(expectedTwoParams, c.ParamValues()) c.SetParamNames("1") assert.Equal(2, *e.maxParam) // Here for backward compatibility the ParamValues remains as they are assert.EqualValues(expectedOneParam, c.ParamValues()) c.SetParamNames("1", "2", "3") assert.Equal(3, *e.maxParam) // Here for backward compatibility the ParamValues remains as they are, but the len is extended to e.maxParam assert.EqualValues(expectedThreeParams, c.ParamValues()) c.SetParamValues("A", "B", "C", "D") assert.Equal(3, *e.maxParam) // Here D shouldn't be returned assert.EqualValues(expectedABCParams, c.ParamValues()) } func TestContextFormValue(t *testing.T) { f := make(url.Values) f.Set("name", "Jon Snow") f.Set("email", "jon@labstack.com") e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Add(HeaderContentType, MIMEApplicationForm) c := e.NewContext(req, nil) // FormValue testify.Equal(t, "Jon Snow", c.FormValue("name")) testify.Equal(t, "jon@labstack.com", c.FormValue("email")) // FormParams params, err := c.FormParams() if testify.NoError(t, err) { testify.Equal(t, url.Values{ "name": []string{"Jon Snow"}, "email": []string{"jon@labstack.com"}, }, params) } // Multipart FormParams error req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Add(HeaderContentType, MIMEMultipartForm) c = e.NewContext(req, nil) params, err = c.FormParams() testify.Nil(t, params) testify.Error(t, err) } func TestContextQueryParam(t *testing.T) { q := make(url.Values) q.Set("name", "Jon Snow") q.Set("email", "jon@labstack.com") req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) e := New() c := e.NewContext(req, nil) // QueryParam testify.Equal(t, "Jon Snow", c.QueryParam("name")) testify.Equal(t, "jon@labstack.com", c.QueryParam("email")) // QueryParams testify.Equal(t, url.Values{ "name": []string{"Jon Snow"}, "email": []string{"jon@labstack.com"}, }, c.QueryParams()) } func TestContextFormFile(t *testing.T) { e := New() buf := new(bytes.Buffer) mr := multipart.NewWriter(buf) w, err := mr.CreateFormFile("file", "test") if testify.NoError(t, err) { w.Write([]byte("test")) } mr.Close() req := httptest.NewRequest(http.MethodPost, "/", buf) req.Header.Set(HeaderContentType, mr.FormDataContentType()) rec := httptest.NewRecorder() c := e.NewContext(req, rec) f, err := c.FormFile("file") if testify.NoError(t, err) { testify.Equal(t, "test", f.Filename) } } func TestContextMultipartForm(t *testing.T) { e := New() buf := new(bytes.Buffer) mw := multipart.NewWriter(buf) mw.WriteField("name", "Jon Snow") mw.Close() req := httptest.NewRequest(http.MethodPost, "/", buf) req.Header.Set(HeaderContentType, mw.FormDataContentType()) rec := httptest.NewRecorder() c := e.NewContext(req, rec) f, err := c.MultipartForm() if testify.NoError(t, err) { testify.NotNil(t, f) } } func TestContextRedirect(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) testify.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo")) testify.Equal(t, http.StatusMovedPermanently, rec.Code) testify.Equal(t, "http://labstack.github.io/echo", rec.Header().Get(HeaderLocation)) testify.Error(t, c.Redirect(310, "http://labstack.github.io/echo")) } func TestContextStore(t *testing.T) { var c Context = new(context) c.Set("name", "Jon Snow") testify.Equal(t, "Jon Snow", c.Get("name")) } func BenchmarkContext_Store(b *testing.B) { e := &Echo{} c := &context{ echo: e, } for n := 0; n < b.N; n++ { c.Set("name", "Jon Snow") if c.Get("name") != "Jon Snow" { b.Fail() } } } func TestContextHandler(t *testing.T) { e := New() r := e.Router() b := new(bytes.Buffer) r.Add(http.MethodGet, "/handler", func(Context) error { _, err := b.Write([]byte("handler")) return err }) c := e.NewContext(nil, nil) r.Find(http.MethodGet, "/handler", c) err := c.Handler()(c) testify.Equal(t, "handler", b.String()) testify.NoError(t, err) } func TestContext_SetHandler(t *testing.T) { var c Context = new(context) testify.Nil(t, c.Handler()) c.SetHandler(func(c Context) error { return nil }) testify.NotNil(t, c.Handler()) } func TestContext_Path(t *testing.T) { path := "/pa/th" var c Context = new(context) c.SetPath(path) testify.Equal(t, path, c.Path()) } type validator struct{} func (*validator) Validate(i interface{}) error { return nil } func TestContext_Validate(t *testing.T) { e := New() c := e.NewContext(nil, nil) testify.Error(t, c.Validate(struct{}{})) e.Validator = &validator{} testify.NoError(t, c.Validate(struct{}{})) } func TestContext_QueryString(t *testing.T) { e := New() queryString := "query=string&var=val" req := httptest.NewRequest(http.MethodGet, "/?"+queryString, nil) c := e.NewContext(req, nil) testify.Equal(t, queryString, c.QueryString()) } func TestContext_Request(t *testing.T) { var c Context = new(context) testify.Nil(t, c.Request()) req := httptest.NewRequest(http.MethodGet, "/path", nil) c.SetRequest(req) testify.Equal(t, req, c.Request()) } func TestContext_Scheme(t *testing.T) { tests := []struct { c Context s string }{ { &context{ request: &http.Request{ TLS: &tls.ConnectionState{}, }, }, "https", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedProto: []string{"https"}}, }, }, "https", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedProtocol: []string{"http"}}, }, }, "http", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedSsl: []string{"on"}}, }, }, "https", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXUrlScheme: []string{"https"}}, }, }, "https", }, { &context{ request: &http.Request{}, }, "http", }, } for _, tt := range tests { testify.Equal(t, tt.s, tt.c.Scheme()) } } func TestContext_IsWebSocket(t *testing.T) { tests := []struct { c Context ws testify.BoolAssertionFunc }{ { &context{ request: &http.Request{ Header: http.Header{HeaderUpgrade: []string{"websocket"}}, }, }, testify.True, }, { &context{ request: &http.Request{ Header: http.Header{HeaderUpgrade: []string{"Websocket"}}, }, }, testify.True, }, { &context{ request: &http.Request{}, }, testify.False, }, { &context{ request: &http.Request{ Header: http.Header{HeaderUpgrade: []string{"other"}}, }, }, testify.False, }, } for i, tt := range tests { t.Run(fmt.Sprintf("test %d", i+1), func(t *testing.T) { tt.ws(t, tt.c.IsWebSocket()) }) } } func TestContext_Bind(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) c := e.NewContext(req, nil) u := new(user) req.Header.Add(HeaderContentType, MIMEApplicationJSON) err := c.Bind(u) testify.NoError(t, err) testify.Equal(t, &user{1, "Jon Snow"}, u) } func TestContext_Logger(t *testing.T) { e := New() c := e.NewContext(nil, nil) log1 := c.Logger() testify.NotNil(t, log1) log2 := log.New("echo2") c.SetLogger(log2) testify.Equal(t, log2, c.Logger()) // Resetting the context returns the initial logger c.Reset(nil, nil) testify.Equal(t, log1, c.Logger()) } func TestContext_RealIP(t *testing.T) { tests := []struct { c Context s string }{ { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1, 127.0.1.1, "}}, }, }, "127.0.0.1", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1,127.0.1.1"}}, }, }, "127.0.0.1", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1"}}, }, }, "127.0.0.1", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348], 2001:db8::1, "}}, }, }, "2001:db8:85a3:8d3:1319:8a2e:370:7348", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348],[2001:db8::1]"}}, }, }, "2001:db8:85a3:8d3:1319:8a2e:370:7348", }, { &context{ request: &http.Request{ Header: http.Header{HeaderXForwardedFor: []string{"2001:db8:85a3:8d3:1319:8a2e:370:7348"}}, }, }, "2001:db8:85a3:8d3:1319:8a2e:370:7348", }, { &context{ request: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{"192.168.0.1"}, }, }, }, "192.168.0.1", }, { &context{ request: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{"[2001:db8::1]"}, }, }, }, "2001:db8::1", }, { &context{ request: &http.Request{ RemoteAddr: "89.89.89.89:1654", }, }, "89.89.89.89", }, } for _, tt := range tests { testify.Equal(t, tt.s, tt.c.RealIP()) } } golang-github-labstack-echo-4.11.1/echo.go000066400000000000000000001035141445647333300203100ustar00rootroot00000000000000/* Package echo implements high performance, minimalist Go web framework. Example: package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) // Handler func hello(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") } func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", hello) // Start server e.Logger.Fatal(e.Start(":1323")) } Learn more at https://echo.labstack.com */ package echo import ( stdContext "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" stdLog "log" "net" "net/http" "os" "reflect" "runtime" "sync" "time" "github.com/labstack/gommon/color" "github.com/labstack/gommon/log" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) type ( // Echo is the top-level framework instance. // // Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these // fields from handlers/middlewares and changing field values at the same time leads to data-races. // Adding new routes after the server has been started is also not safe! Echo struct { filesystem common // startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get // listener address info (on which interface/port was listener binded) without having data races. startupMutex sync.RWMutex colorer *color.Color // premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns // an error the router is not executed and the request will end up in the global error handler. premiddleware []MiddlewareFunc middleware []MiddlewareFunc maxParam *int router *Router routers map[string]*Router pool sync.Pool StdLogger *stdLog.Logger Server *http.Server TLSServer *http.Server Listener net.Listener TLSListener net.Listener AutoTLSManager autocert.Manager DisableHTTP2 bool Debug bool HideBanner bool HidePort bool HTTPErrorHandler HTTPErrorHandler Binder Binder JSONSerializer JSONSerializer Validator Validator Renderer Renderer Logger Logger IPExtractor IPExtractor ListenerNetwork string // OnAddRouteHandler is called when Echo adds new route to specific host router. OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc) } // Route contains a handler and information for matching against requests. Route struct { Method string `json:"method"` Path string `json:"path"` Name string `json:"name"` } // HTTPError represents an error that occurred while handling a request. HTTPError struct { Code int `json:"-"` Message interface{} `json:"message"` Internal error `json:"-"` // Stores the error returned by an external dependency } // MiddlewareFunc defines a function to process middleware. MiddlewareFunc func(next HandlerFunc) HandlerFunc // HandlerFunc defines a function to serve HTTP requests. HandlerFunc func(c Context) error // HTTPErrorHandler is a centralized HTTP error handler. HTTPErrorHandler func(err error, c Context) // Validator is the interface that wraps the Validate function. Validator interface { Validate(i interface{}) error } // JSONSerializer is the interface that encodes and decodes JSON to and from interfaces. JSONSerializer interface { Serialize(c Context, i interface{}, indent string) error Deserialize(c Context, i interface{}) error } // Renderer is the interface that wraps the Render function. Renderer interface { Render(io.Writer, string, interface{}, Context) error } // Map defines a generic map of type `map[string]interface{}`. Map map[string]interface{} // Common struct for Echo & Group. common struct{} ) // HTTP methods // NOTE: Deprecated, please use the stdlib constants directly instead. const ( CONNECT = http.MethodConnect DELETE = http.MethodDelete GET = http.MethodGet HEAD = http.MethodHead OPTIONS = http.MethodOptions PATCH = http.MethodPatch POST = http.MethodPost // PROPFIND = "PROPFIND" PUT = http.MethodPut TRACE = http.MethodTrace ) // MIME types const ( MIMEApplicationJSON = "application/json" MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8 MIMEApplicationJavaScript = "application/javascript" MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 MIMEApplicationXML = "application/xml" MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8 MIMETextXML = "text/xml" MIMETextXMLCharsetUTF8 = MIMETextXML + "; " + charsetUTF8 MIMEApplicationForm = "application/x-www-form-urlencoded" MIMEApplicationProtobuf = "application/protobuf" MIMEApplicationMsgpack = "application/msgpack" MIMETextHTML = "text/html" MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8 MIMETextPlain = "text/plain" MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8 MIMEMultipartForm = "multipart/form-data" MIMEOctetStream = "application/octet-stream" ) const ( charsetUTF8 = "charset=UTF-8" // PROPFIND Method can be used on collection and property resources. PROPFIND = "PROPFIND" // REPORT Method can be used to get information about a resource, see rfc 3253 REPORT = "REPORT" // RouteNotFound is special method type for routes handling "route not found" (404) cases RouteNotFound = "echo_route_not_found" ) // Headers const ( HeaderAccept = "Accept" HeaderAcceptEncoding = "Accept-Encoding" // HeaderAllow is the name of the "Allow" header field used to list the set of methods // advertised as supported by the target resource. Returning an Allow header is mandatory // for status 405 (method not found) and useful for the OPTIONS method in responses. // See RFC 7231: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1 HeaderAllow = "Allow" HeaderAuthorization = "Authorization" HeaderContentDisposition = "Content-Disposition" HeaderContentEncoding = "Content-Encoding" HeaderContentLength = "Content-Length" HeaderContentType = "Content-Type" HeaderCookie = "Cookie" HeaderSetCookie = "Set-Cookie" HeaderIfModifiedSince = "If-Modified-Since" HeaderLastModified = "Last-Modified" HeaderLocation = "Location" HeaderRetryAfter = "Retry-After" HeaderUpgrade = "Upgrade" HeaderVary = "Vary" HeaderWWWAuthenticate = "WWW-Authenticate" HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedProto = "X-Forwarded-Proto" HeaderXForwardedProtocol = "X-Forwarded-Protocol" HeaderXForwardedSsl = "X-Forwarded-Ssl" HeaderXUrlScheme = "X-Url-Scheme" HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXRealIP = "X-Real-Ip" HeaderXRequestID = "X-Request-Id" HeaderXCorrelationID = "X-Correlation-Id" HeaderXRequestedWith = "X-Requested-With" HeaderServer = "Server" HeaderOrigin = "Origin" HeaderCacheControl = "Cache-Control" HeaderConnection = "Connection" // Access control HeaderAccessControlRequestMethod = "Access-Control-Request-Method" HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" HeaderAccessControlMaxAge = "Access-Control-Max-Age" // Security HeaderStrictTransportSecurity = "Strict-Transport-Security" HeaderXContentTypeOptions = "X-Content-Type-Options" HeaderXXSSProtection = "X-XSS-Protection" HeaderXFrameOptions = "X-Frame-Options" HeaderContentSecurityPolicy = "Content-Security-Policy" HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" HeaderXCSRFToken = "X-CSRF-Token" HeaderReferrerPolicy = "Referrer-Policy" ) const ( // Version of Echo Version = "4.11.1" website = "https://echo.labstack.com" // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo banner = ` ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ %s High performance, minimalist Go web framework %s ____________________________________O/_______ O\ ` ) var ( methods = [...]string{ http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodPost, PROPFIND, http.MethodPut, http.MethodTrace, REPORT, } ) // Errors var ( ErrBadRequest = NewHTTPError(http.StatusBadRequest) // HTTP 400 Bad Request ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) // HTTP 401 Unauthorized ErrPaymentRequired = NewHTTPError(http.StatusPaymentRequired) // HTTP 402 Payment Required ErrForbidden = NewHTTPError(http.StatusForbidden) // HTTP 403 Forbidden ErrNotFound = NewHTTPError(http.StatusNotFound) // HTTP 404 Not Found ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) // HTTP 405 Method Not Allowed ErrNotAcceptable = NewHTTPError(http.StatusNotAcceptable) // HTTP 406 Not Acceptable ErrProxyAuthRequired = NewHTTPError(http.StatusProxyAuthRequired) // HTTP 407 Proxy AuthRequired ErrRequestTimeout = NewHTTPError(http.StatusRequestTimeout) // HTTP 408 Request Timeout ErrConflict = NewHTTPError(http.StatusConflict) // HTTP 409 Conflict ErrGone = NewHTTPError(http.StatusGone) // HTTP 410 Gone ErrLengthRequired = NewHTTPError(http.StatusLengthRequired) // HTTP 411 Length Required ErrPreconditionFailed = NewHTTPError(http.StatusPreconditionFailed) // HTTP 412 Precondition Failed ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) // HTTP 413 Payload Too Large ErrRequestURITooLong = NewHTTPError(http.StatusRequestURITooLong) // HTTP 414 URI Too Long ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) // HTTP 415 Unsupported Media Type ErrRequestedRangeNotSatisfiable = NewHTTPError(http.StatusRequestedRangeNotSatisfiable) // HTTP 416 Range Not Satisfiable ErrExpectationFailed = NewHTTPError(http.StatusExpectationFailed) // HTTP 417 Expectation Failed ErrTeapot = NewHTTPError(http.StatusTeapot) // HTTP 418 I'm a teapot ErrMisdirectedRequest = NewHTTPError(http.StatusMisdirectedRequest) // HTTP 421 Misdirected Request ErrUnprocessableEntity = NewHTTPError(http.StatusUnprocessableEntity) // HTTP 422 Unprocessable Entity ErrLocked = NewHTTPError(http.StatusLocked) // HTTP 423 Locked ErrFailedDependency = NewHTTPError(http.StatusFailedDependency) // HTTP 424 Failed Dependency ErrTooEarly = NewHTTPError(http.StatusTooEarly) // HTTP 425 Too Early ErrUpgradeRequired = NewHTTPError(http.StatusUpgradeRequired) // HTTP 426 Upgrade Required ErrPreconditionRequired = NewHTTPError(http.StatusPreconditionRequired) // HTTP 428 Precondition Required ErrTooManyRequests = NewHTTPError(http.StatusTooManyRequests) // HTTP 429 Too Many Requests ErrRequestHeaderFieldsTooLarge = NewHTTPError(http.StatusRequestHeaderFieldsTooLarge) // HTTP 431 Request Header Fields Too Large ErrUnavailableForLegalReasons = NewHTTPError(http.StatusUnavailableForLegalReasons) // HTTP 451 Unavailable For Legal Reasons ErrInternalServerError = NewHTTPError(http.StatusInternalServerError) // HTTP 500 Internal Server Error ErrNotImplemented = NewHTTPError(http.StatusNotImplemented) // HTTP 501 Not Implemented ErrBadGateway = NewHTTPError(http.StatusBadGateway) // HTTP 502 Bad Gateway ErrServiceUnavailable = NewHTTPError(http.StatusServiceUnavailable) // HTTP 503 Service Unavailable ErrGatewayTimeout = NewHTTPError(http.StatusGatewayTimeout) // HTTP 504 Gateway Timeout ErrHTTPVersionNotSupported = NewHTTPError(http.StatusHTTPVersionNotSupported) // HTTP 505 HTTP Version Not Supported ErrVariantAlsoNegotiates = NewHTTPError(http.StatusVariantAlsoNegotiates) // HTTP 506 Variant Also Negotiates ErrInsufficientStorage = NewHTTPError(http.StatusInsufficientStorage) // HTTP 507 Insufficient Storage ErrLoopDetected = NewHTTPError(http.StatusLoopDetected) // HTTP 508 Loop Detected ErrNotExtended = NewHTTPError(http.StatusNotExtended) // HTTP 510 Not Extended ErrNetworkAuthenticationRequired = NewHTTPError(http.StatusNetworkAuthenticationRequired) // HTTP 511 Network Authentication Required ErrValidatorNotRegistered = errors.New("validator not registered") ErrRendererNotRegistered = errors.New("renderer not registered") ErrInvalidRedirectCode = errors.New("invalid redirect status code") ErrCookieNotFound = errors.New("cookie not found") ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte") ErrInvalidListenerNetwork = errors.New("invalid listener network") ) // Error handlers var ( NotFoundHandler = func(c Context) error { return ErrNotFound } MethodNotAllowedHandler = func(c Context) error { // See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed) // response and MAY do so in any other response. For disabled resources an empty Allow header may be returned routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string) if ok && routerAllowMethods != "" { c.Response().Header().Set(HeaderAllow, routerAllowMethods) } return ErrMethodNotAllowed } ) // New creates an instance of Echo. func New() (e *Echo) { e = &Echo{ filesystem: createFilesystem(), Server: new(http.Server), TLSServer: new(http.Server), AutoTLSManager: autocert.Manager{ Prompt: autocert.AcceptTOS, }, Logger: log.New("echo"), colorer: color.New(), maxParam: new(int), ListenerNetwork: "tcp", } e.Server.Handler = e e.TLSServer.Handler = e e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.Binder = &DefaultBinder{} e.JSONSerializer = &DefaultJSONSerializer{} e.Logger.SetLevel(log.ERROR) e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.pool.New = func() interface{} { return e.NewContext(nil, nil) } e.router = NewRouter(e) e.routers = map[string]*Router{} return } // NewContext returns a Context instance. func (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) Context { return &context{ request: r, response: NewResponse(w, e), store: make(Map), echo: e, pvalues: make([]string, *e.maxParam), handler: NotFoundHandler, } } // Router returns the default router. func (e *Echo) Router() *Router { return e.router } // Routers returns the map of host => router. func (e *Echo) Routers() map[string]*Router { return e.routers } // DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response // with status code. // // NOTE: In case errors happens in middleware call-chain that is returning from handler (which did not return an error). // When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from // handler. Then the error that global error handler received will be ignored because we have already "commited" the // response and status code header has been sent to the client. func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { if c.Response().Committed { return } he, ok := err.(*HTTPError) if ok { if he.Internal != nil { if herr, ok := he.Internal.(*HTTPError); ok { he = herr } } } else { he = &HTTPError{ Code: http.StatusInternalServerError, Message: http.StatusText(http.StatusInternalServerError), } } // Issue #1426 code := he.Code message := he.Message switch m := he.Message.(type) { case string: if e.Debug { message = Map{"message": m, "error": err.Error()} } else { message = Map{"message": m} } case json.Marshaler: // do nothing - this type knows how to format itself to JSON case error: message = Map{"message": m.Error()} } // Send response if c.Request().Method == http.MethodHead { // Issue #608 err = c.NoContent(he.Code) } else { err = c.JSON(code, message) } if err != nil { e.Logger.Error(err) } } // Pre adds middleware to the chain which is run before router. func (e *Echo) Pre(middleware ...MiddlewareFunc) { e.premiddleware = append(e.premiddleware, middleware...) } // Use adds middleware to the chain which is run after router. func (e *Echo) Use(middleware ...MiddlewareFunc) { e.middleware = append(e.middleware, middleware...) } // CONNECT registers a new CONNECT route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodConnect, path, h, m...) } // DELETE registers a new DELETE route for a path with matching handler in the router // with optional route-level middleware. func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodDelete, path, h, m...) } // GET registers a new GET route for a path with matching handler in the router // with optional route-level middleware. func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodGet, path, h, m...) } // HEAD registers a new HEAD route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodHead, path, h, m...) } // OPTIONS registers a new OPTIONS route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodOptions, path, h, m...) } // PATCH registers a new PATCH route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodPatch, path, h, m...) } // POST registers a new POST route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodPost, path, h, m...) } // PUT registers a new PUT route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodPut, path, h, m...) } // TRACE registers a new TRACE route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(http.MethodTrace, path, h, m...) } // RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases) // for current request URL. // Path supports static and named/any parameters just like other http method is defined. Generally path is ended with // wildcard/match-any character (`/*`, `/download/*` etc). // // Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return e.Add(RouteNotFound, path, h, m...) } // Any registers a new route for all HTTP methods (supported by Echo) and path with matching handler // in the router with optional route-level middleware. // // Note: this method only adds specific set of supported HTTP methods as handler and is not true // "catch-any-arbitrary-method" way of matching requests. func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { routes := make([]*Route, len(methods)) for i, m := range methods { routes[i] = e.Add(m, path, handler, middleware...) } return routes } // Match registers a new route for multiple HTTP methods and path with matching // handler in the router with optional route-level middleware. func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { routes := make([]*Route, len(methods)) for i, m := range methods { routes[i] = e.Add(m, path, handler, middleware...) } return routes } func (common) file(path, file string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route, m ...MiddlewareFunc) *Route { return get(path, func(c Context) error { return c.File(file) }, m...) } // File registers a new route with path to serve a static file with optional route-level middleware. func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route { return e.file(path, file, e.GET, m...) } func (e *Echo) add(host, method, path string, handler HandlerFunc, middlewares ...MiddlewareFunc) *Route { router := e.findRouter(host) //FIXME: when handler+middleware are both nil ... make it behave like handler removal name := handlerName(handler) route := router.add(method, path, name, func(c Context) error { h := applyMiddleware(handler, middlewares...) return h(c) }) if e.OnAddRouteHandler != nil { e.OnAddRouteHandler(host, *route, handler, middlewares) } return route } // Add registers a new route for an HTTP method and path with matching handler // in the router with optional route-level middleware. func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route { return e.add("", method, path, handler, middleware...) } // Host creates a new router group for the provided host and optional host-level middleware. func (e *Echo) Host(name string, m ...MiddlewareFunc) (g *Group) { e.routers[name] = NewRouter(e) g = &Group{host: name, echo: e} g.Use(m...) return } // Group creates a new router group with prefix and optional group-level middleware. func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) { g = &Group{prefix: prefix, echo: e} g.Use(m...) return } // URI generates an URI from handler. func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { name := handlerName(handler) return e.Reverse(name, params...) } // URL is an alias for `URI` function. func (e *Echo) URL(h HandlerFunc, params ...interface{}) string { return e.URI(h, params...) } // Reverse generates a URL from route name and provided parameters. func (e *Echo) Reverse(name string, params ...interface{}) string { return e.router.Reverse(name, params...) } // Routes returns the registered routes for default router. // In case when Echo serves multiple hosts/domains use `e.Routers()["domain2.site"].Routes()` to get specific host routes. func (e *Echo) Routes() []*Route { return e.router.Routes() } // AcquireContext returns an empty `Context` instance from the pool. // You must return the context by calling `ReleaseContext()`. func (e *Echo) AcquireContext() Context { return e.pool.Get().(Context) } // ReleaseContext returns the `Context` instance back to the pool. // You must call it after `AcquireContext()`. func (e *Echo) ReleaseContext(c Context) { e.pool.Put(c) } // ServeHTTP implements `http.Handler` interface, which serves HTTP requests. func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Acquire context c := e.pool.Get().(*context) c.Reset(r, w) var h HandlerFunc if e.premiddleware == nil { e.findRouter(r.Host).Find(r.Method, GetPath(r), c) h = c.Handler() h = applyMiddleware(h, e.middleware...) } else { h = func(c Context) error { e.findRouter(r.Host).Find(r.Method, GetPath(r), c) h := c.Handler() h = applyMiddleware(h, e.middleware...) return h(c) } h = applyMiddleware(h, e.premiddleware...) } // Execute chain if err := h(c); err != nil { e.HTTPErrorHandler(err, c) } // Release context e.pool.Put(c) } // Start starts an HTTP server. func (e *Echo) Start(address string) error { e.startupMutex.Lock() e.Server.Addr = address if err := e.configureServer(e.Server); err != nil { e.startupMutex.Unlock() return err } e.startupMutex.Unlock() return e.Server.Serve(e.Listener) } // StartTLS starts an HTTPS server. // If `certFile` or `keyFile` is `string` the values are treated as file paths. // If `certFile` or `keyFile` is `[]byte` the values are treated as the certificate or key as-is. func (e *Echo) StartTLS(address string, certFile, keyFile interface{}) (err error) { e.startupMutex.Lock() var cert []byte if cert, err = filepathOrContent(certFile); err != nil { e.startupMutex.Unlock() return } var key []byte if key, err = filepathOrContent(keyFile); err != nil { e.startupMutex.Unlock() return } s := e.TLSServer s.TLSConfig = new(tls.Config) s.TLSConfig.Certificates = make([]tls.Certificate, 1) if s.TLSConfig.Certificates[0], err = tls.X509KeyPair(cert, key); err != nil { e.startupMutex.Unlock() return } e.configureTLS(address) if err := e.configureServer(s); err != nil { e.startupMutex.Unlock() return err } e.startupMutex.Unlock() return s.Serve(e.TLSListener) } func filepathOrContent(fileOrContent interface{}) (content []byte, err error) { switch v := fileOrContent.(type) { case string: return os.ReadFile(v) case []byte: return v, nil default: return nil, ErrInvalidCertOrKeyType } } // StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org. func (e *Echo) StartAutoTLS(address string) error { e.startupMutex.Lock() s := e.TLSServer s.TLSConfig = new(tls.Config) s.TLSConfig.GetCertificate = e.AutoTLSManager.GetCertificate s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) e.configureTLS(address) if err := e.configureServer(s); err != nil { e.startupMutex.Unlock() return err } e.startupMutex.Unlock() return s.Serve(e.TLSListener) } func (e *Echo) configureTLS(address string) { s := e.TLSServer s.Addr = address if !e.DisableHTTP2 { s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "h2") } } // StartServer starts a custom http server. func (e *Echo) StartServer(s *http.Server) (err error) { e.startupMutex.Lock() if err := e.configureServer(s); err != nil { e.startupMutex.Unlock() return err } if s.TLSConfig != nil { e.startupMutex.Unlock() return s.Serve(e.TLSListener) } e.startupMutex.Unlock() return s.Serve(e.Listener) } func (e *Echo) configureServer(s *http.Server) error { // Setup e.colorer.SetOutput(e.Logger.Output()) s.ErrorLog = e.StdLogger s.Handler = e if e.Debug { e.Logger.SetLevel(log.DEBUG) } if !e.HideBanner { e.colorer.Printf(banner, e.colorer.Red("v"+Version), e.colorer.Blue(website)) } if s.TLSConfig == nil { if e.Listener == nil { l, err := newListener(s.Addr, e.ListenerNetwork) if err != nil { return err } e.Listener = l } if !e.HidePort { e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) } return nil } if e.TLSListener == nil { l, err := newListener(s.Addr, e.ListenerNetwork) if err != nil { return err } e.TLSListener = tls.NewListener(l, s.TLSConfig) } if !e.HidePort { e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) } return nil } // ListenerAddr returns net.Addr for Listener func (e *Echo) ListenerAddr() net.Addr { e.startupMutex.RLock() defer e.startupMutex.RUnlock() if e.Listener == nil { return nil } return e.Listener.Addr() } // TLSListenerAddr returns net.Addr for TLSListener func (e *Echo) TLSListenerAddr() net.Addr { e.startupMutex.RLock() defer e.startupMutex.RUnlock() if e.TLSListener == nil { return nil } return e.TLSListener.Addr() } // StartH2CServer starts a custom http/2 server with h2c (HTTP/2 Cleartext). func (e *Echo) StartH2CServer(address string, h2s *http2.Server) error { e.startupMutex.Lock() // Setup s := e.Server s.Addr = address e.colorer.SetOutput(e.Logger.Output()) s.ErrorLog = e.StdLogger s.Handler = h2c.NewHandler(e, h2s) if e.Debug { e.Logger.SetLevel(log.DEBUG) } if !e.HideBanner { e.colorer.Printf(banner, e.colorer.Red("v"+Version), e.colorer.Blue(website)) } if e.Listener == nil { l, err := newListener(s.Addr, e.ListenerNetwork) if err != nil { e.startupMutex.Unlock() return err } e.Listener = l } if !e.HidePort { e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) } e.startupMutex.Unlock() return s.Serve(e.Listener) } // Close immediately stops the server. // It internally calls `http.Server#Close()`. func (e *Echo) Close() error { e.startupMutex.Lock() defer e.startupMutex.Unlock() if err := e.TLSServer.Close(); err != nil { return err } return e.Server.Close() } // Shutdown stops the server gracefully. // It internally calls `http.Server#Shutdown()`. func (e *Echo) Shutdown(ctx stdContext.Context) error { e.startupMutex.Lock() defer e.startupMutex.Unlock() if err := e.TLSServer.Shutdown(ctx); err != nil { return err } return e.Server.Shutdown(ctx) } // NewHTTPError creates a new HTTPError instance. func NewHTTPError(code int, message ...interface{}) *HTTPError { he := &HTTPError{Code: code, Message: http.StatusText(code)} if len(message) > 0 { he.Message = message[0] } return he } // Error makes it compatible with `error` interface. func (he *HTTPError) Error() string { if he.Internal == nil { return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message) } return fmt.Sprintf("code=%d, message=%v, internal=%v", he.Code, he.Message, he.Internal) } // SetInternal sets error to HTTPError.Internal func (he *HTTPError) SetInternal(err error) *HTTPError { he.Internal = err return he } // WithInternal returns clone of HTTPError with err set to HTTPError.Internal field func (he *HTTPError) WithInternal(err error) *HTTPError { return &HTTPError{ Code: he.Code, Message: he.Message, Internal: err, } } // Unwrap satisfies the Go 1.13 error wrapper interface. func (he *HTTPError) Unwrap() error { return he.Internal } // WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. func WrapHandler(h http.Handler) HandlerFunc { return func(c Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } } // WrapMiddleware wraps `func(http.Handler) http.Handler` into `echo.MiddlewareFunc` func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc { return func(next HandlerFunc) HandlerFunc { return func(c Context) (err error) { m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.SetRequest(r) c.SetResponse(NewResponse(w, c.Echo())) err = next(c) })).ServeHTTP(c.Response(), c.Request()) return } } } // GetPath returns RawPath, if it's empty returns Path from URL // Difference between RawPath and Path is: // - Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/. // - RawPath is an optional field which only gets set if the default encoding is different from Path. func GetPath(r *http.Request) string { path := r.URL.RawPath if path == "" { path = r.URL.Path } return path } func (e *Echo) findRouter(host string) *Router { if len(e.routers) > 0 { if r, ok := e.routers[host]; ok { return r } } return e.router } func handlerName(h HandlerFunc) string { t := reflect.ValueOf(h).Type() if t.Kind() == reflect.Func { return runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() } return t.String() } // // PathUnescape is wraps `url.PathUnescape` // func PathUnescape(s string) (string, error) { // return url.PathUnescape(s) // } // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually // go away. type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { if c, err = ln.AcceptTCP(); err != nil { return } else if err = c.(*net.TCPConn).SetKeepAlive(true); err != nil { return } // Ignore error from setting the KeepAlivePeriod as some systems, such as // OpenBSD, do not support setting TCP_USER_TIMEOUT on IPPROTO_TCP _ = c.(*net.TCPConn).SetKeepAlivePeriod(3 * time.Minute) return } func newListener(address, network string) (*tcpKeepAliveListener, error) { if network != "tcp" && network != "tcp4" && network != "tcp6" { return nil, ErrInvalidListenerNetwork } l, err := net.Listen(network, address) if err != nil { return nil, err } return &tcpKeepAliveListener{l.(*net.TCPListener)}, nil } func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc { for i := len(middleware) - 1; i >= 0; i-- { h = middleware[i](h) } return h } golang-github-labstack-echo-4.11.1/echo_fs.go000066400000000000000000000134041445647333300207760ustar00rootroot00000000000000package echo import ( "fmt" "io/fs" "net/http" "net/url" "os" "path/filepath" "strings" ) type filesystem struct { // Filesystem is file system used by Static and File handlers to access files. // Defaults to os.DirFS(".") // // When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary // prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths // including `assets/images` as their prefix. Filesystem fs.FS } func createFilesystem() filesystem { return filesystem{ Filesystem: newDefaultFS(), } } // Static registers a new route with path prefix to serve static files from the provided root directory. func (e *Echo) Static(pathPrefix, fsRoot string) *Route { subFs := MustSubFS(e.Filesystem, fsRoot) return e.Add( http.MethodGet, pathPrefix+"*", StaticDirectoryHandler(subFs, false), ) } // StaticFS registers a new route with path prefix to serve static files from the provided file system. // // When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary // prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths // including `assets/images` as their prefix. func (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS) *Route { return e.Add( http.MethodGet, pathPrefix+"*", StaticDirectoryHandler(filesystem, false), ) } // StaticDirectoryHandler creates handler function to serve files from provided file system // When disablePathUnescaping is set then file name from path is not unescaped and is served as is. func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc { return func(c Context) error { p := c.Param("*") if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice tmpPath, err := url.PathUnescape(p) if err != nil { return fmt.Errorf("failed to unescape path variable: %w", err) } p = tmpPath } // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/"))) fi, err := fs.Stat(fileSystem, name) if err != nil { return ErrNotFound } // If the request is for a directory and does not end with "/" p = c.Request().URL.Path // path must not be empty. if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' { // Redirect to ends with "/" return c.Redirect(http.StatusMovedPermanently, sanitizeURI(p+"/")) } return fsFile(c, name, fileSystem) } } // FileFS registers a new route with path to serve file from the provided file system. func (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route { return e.GET(path, StaticFileHandler(file, filesystem), m...) } // StaticFileHandler creates handler function to serve file from provided file system func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc { return func(c Context) error { return fsFile(c, file, filesystem) } } // defaultFS exists to preserve pre v4.7.0 behaviour where files were open by `os.Open`. // v4.7 introduced `echo.Filesystem` field which is Go1.16+ `fs.Fs` interface. // Difference between `os.Open` and `fs.Open` is that FS does not allow opening path that start with `.`, `..` or `/` // etc. For example previously you could have `../images` in your application but `fs := os.DirFS("./")` would not // allow you to use `fs.Open("../images")` and this would break all old applications that rely on being able to // traverse up from current executable run path. // NB: private because you really should use fs.FS implementation instances type defaultFS struct { prefix string fs fs.FS } func newDefaultFS() *defaultFS { dir, _ := os.Getwd() return &defaultFS{ prefix: dir, fs: nil, } } func (fs defaultFS) Open(name string) (fs.File, error) { if fs.fs == nil { return os.Open(name) } return fs.fs.Open(name) } func subFS(currentFs fs.FS, root string) (fs.FS, error) { root = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows if dFS, ok := currentFs.(*defaultFS); ok { // we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS. // fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we // were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs if !filepath.IsAbs(root) { root = filepath.Join(dFS.prefix, root) } return &defaultFS{ prefix: root, fs: os.DirFS(root), }, nil } return fs.Sub(currentFs, root) } // MustSubFS creates sub FS from current filesystem or panic on failure. // Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules. // // MustSubFS is helpful when dealing with `embed.FS` because for example `//go:embed assets/images` embeds files with // paths including `assets/images` as their prefix. In that case use `fs := echo.MustSubFS(fs, "rootDirectory") to // create sub fs which uses necessary prefix for directory path. func MustSubFS(currentFs fs.FS, fsRoot string) fs.FS { subFs, err := subFS(currentFs, fsRoot) if err != nil { panic(fmt.Errorf("can not create sub FS, invalid root given, err: %w", err)) } return subFs } func sanitizeURI(uri string) string { // double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri // we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') { uri = "/" + strings.TrimLeft(uri, `/\`) } return uri } golang-github-labstack-echo-4.11.1/echo_fs_test.go000066400000000000000000000202221445647333300220310ustar00rootroot00000000000000package echo import ( "github.com/stretchr/testify/assert" "io/fs" "net/http" "net/http/httptest" "os" "strings" "testing" ) func TestEcho_StaticFS(t *testing.T) { var testCases = []struct { name string givenPrefix string givenFs fs.FS givenFsRoot string whenURL string expectStatus int expectHeaderLocation string expectBodyStartsWith string }{ { name: "ok", givenPrefix: "/images", givenFs: os.DirFS("./_fixture/images"), whenURL: "/images/walle.png", expectStatus: http.StatusOK, expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}), }, { name: "ok, from sub fs", givenPrefix: "/images", givenFs: MustSubFS(os.DirFS("./_fixture/"), "images"), whenURL: "/images/walle.png", expectStatus: http.StatusOK, expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}), }, { name: "No file", givenPrefix: "/images", givenFs: os.DirFS("_fixture/scripts"), whenURL: "/images/bolt.png", expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Directory", givenPrefix: "/images", givenFs: os.DirFS("_fixture/images"), whenURL: "/images/", expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Directory Redirect", givenPrefix: "/", givenFs: os.DirFS("_fixture/"), whenURL: "/folder", expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/folder/", expectBodyStartsWith: "", }, { name: "Directory Redirect with non-root path", givenPrefix: "/static", givenFs: os.DirFS("_fixture"), whenURL: "/static", expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/static/", expectBodyStartsWith: "", }, { name: "Prefixed directory 404 (request URL without slash)", givenPrefix: "/folder/", // trailing slash will intentionally not match "/folder" givenFs: os.DirFS("_fixture"), whenURL: "/folder", // no trailing slash expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Prefixed directory redirect (without slash redirect to slash)", givenPrefix: "/folder", // no trailing slash shall match /folder and /folder/* givenFs: os.DirFS("_fixture"), whenURL: "/folder", // no trailing slash expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/folder/", expectBodyStartsWith: "", }, { name: "Directory with index.html", givenPrefix: "/", givenFs: os.DirFS("_fixture"), whenURL: "/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Prefixed directory with index.html (prefix ending with slash)", givenPrefix: "/assets/", givenFs: os.DirFS("_fixture"), whenURL: "/assets/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Prefixed directory with index.html (prefix ending without slash)", givenPrefix: "/assets", givenFs: os.DirFS("_fixture"), whenURL: "/assets/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Sub-directory with index.html", givenPrefix: "/", givenFs: os.DirFS("_fixture"), whenURL: "/folder/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "do not allow directory traversal (backslash - windows separator)", givenPrefix: "/", givenFs: os.DirFS("_fixture/"), whenURL: `/..\\middleware/basic_auth.go`, expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "do not allow directory traversal (slash - unix separator)", givenPrefix: "/", givenFs: os.DirFS("_fixture/"), whenURL: `/../middleware/basic_auth.go`, expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "open redirect vulnerability", givenPrefix: "/", givenFs: os.DirFS("_fixture/"), whenURL: "/open.redirect.hackercom%2f..", expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/open.redirect.hackercom/../", // location starting with `//open` would be very bad expectBodyStartsWith: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() tmpFs := tc.givenFs if tc.givenFsRoot != "" { tmpFs = MustSubFS(tmpFs, tc.givenFsRoot) } e.StaticFS(tc.givenPrefix, tmpFs) req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectStatus, rec.Code) body := rec.Body.String() if tc.expectBodyStartsWith != "" { assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith)) } else { assert.Equal(t, "", body) } if tc.expectHeaderLocation != "" { assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0]) } else { _, ok := rec.Result().Header["Location"] assert.False(t, ok) } }) } } func TestEcho_FileFS(t *testing.T) { var testCases = []struct { name string whenPath string whenFile string whenFS fs.FS givenURL string expectCode int expectStartsWith []byte }{ { name: "ok", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "walle.png", givenURL: "/walle", expectCode: http.StatusOK, expectStartsWith: []byte{0x89, 0x50, 0x4e}, }, { name: "nok, requesting invalid path", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "walle.png", givenURL: "/walle.png", expectCode: http.StatusNotFound, expectStartsWith: []byte(`{"message":"Not Found"}`), }, { name: "nok, serving not existent file from filesystem", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "not-existent.png", givenURL: "/walle", expectCode: http.StatusNotFound, expectStartsWith: []byte(`{"message":"Not Found"}`), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.FileFS(tc.whenPath, tc.whenFile, tc.whenFS) req := httptest.NewRequest(http.MethodGet, tc.givenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectCode, rec.Code) body := rec.Body.Bytes() if len(body) > len(tc.expectStartsWith) { body = body[:len(tc.expectStartsWith)] } assert.Equal(t, tc.expectStartsWith, body) }) } } func TestEcho_StaticPanic(t *testing.T) { var testCases = []struct { name string givenRoot string expectError string }{ { name: "panics for ../", givenRoot: "../assets", expectError: "can not create sub FS, invalid root given, err: sub ../assets: invalid name", }, { name: "panics for /", givenRoot: "/assets", expectError: "can not create sub FS, invalid root given, err: sub /assets: invalid name", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Filesystem = os.DirFS("./") assert.PanicsWithError(t, tc.expectError, func() { e.Static("../assets", tc.givenRoot) }) }) } } golang-github-labstack-echo-4.11.1/echo_test.go000066400000000000000000001357351445647333300213610ustar00rootroot00000000000000package echo import ( "bytes" stdContext "context" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "os" "reflect" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/http2" ) type ( user struct { ID int `json:"id" xml:"id" form:"id" query:"id" param:"id" header:"id"` Name string `json:"name" xml:"name" form:"name" query:"name" param:"name" header:"name"` } ) const ( userJSON = `{"id":1,"name":"Jon Snow"}` usersJSON = `[{"id":1,"name":"Jon Snow"}]` userXML = `1Jon Snow` userForm = `id=1&name=Jon Snow` invalidContent = "invalid content" userJSONInvalidType = `{"id":"1","name":"Jon Snow"}` userXMLConvertNumberError = `Number oneJon Snow` userXMLUnsupportedTypeError = `<>Number oneJon Snow` ) const userJSONPretty = `{ "id": 1, "name": "Jon Snow" }` const userXMLPretty = ` 1 Jon Snow ` var dummyQuery = url.Values{"dummy": []string{"useless"}} func TestEcho(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // Router assert.NotNil(t, e.Router()) // DefaultHTTPErrorHandler e.DefaultHTTPErrorHandler(errors.New("error"), c) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestEchoStatic(t *testing.T) { var testCases = []struct { name string givenPrefix string givenRoot string whenURL string expectStatus int expectHeaderLocation string expectBodyStartsWith string }{ { name: "ok", givenPrefix: "/images", givenRoot: "_fixture/images", whenURL: "/images/walle.png", expectStatus: http.StatusOK, expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}), }, { name: "ok with relative path for root points to directory", givenPrefix: "/images", givenRoot: "./_fixture/images", whenURL: "/images/walle.png", expectStatus: http.StatusOK, expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}), }, { name: "No file", givenPrefix: "/images", givenRoot: "_fixture/scripts", whenURL: "/images/bolt.png", expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Directory", givenPrefix: "/images", givenRoot: "_fixture/images", whenURL: "/images/", expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Directory Redirect", givenPrefix: "/", givenRoot: "_fixture", whenURL: "/folder", expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/folder/", expectBodyStartsWith: "", }, { name: "Directory Redirect with non-root path", givenPrefix: "/static", givenRoot: "_fixture", whenURL: "/static", expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/static/", expectBodyStartsWith: "", }, { name: "Prefixed directory 404 (request URL without slash)", givenPrefix: "/folder/", // trailing slash will intentionally not match "/folder" givenRoot: "_fixture", whenURL: "/folder", // no trailing slash expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "Prefixed directory redirect (without slash redirect to slash)", givenPrefix: "/folder", // no trailing slash shall match /folder and /folder/* givenRoot: "_fixture", whenURL: "/folder", // no trailing slash expectStatus: http.StatusMovedPermanently, expectHeaderLocation: "/folder/", expectBodyStartsWith: "", }, { name: "Directory with index.html", givenPrefix: "/", givenRoot: "_fixture", whenURL: "/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Prefixed directory with index.html (prefix ending with slash)", givenPrefix: "/assets/", givenRoot: "_fixture", whenURL: "/assets/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Prefixed directory with index.html (prefix ending without slash)", givenPrefix: "/assets", givenRoot: "_fixture", whenURL: "/assets/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "Sub-directory with index.html", givenPrefix: "/", givenRoot: "_fixture", whenURL: "/folder/", expectStatus: http.StatusOK, expectBodyStartsWith: "", }, { name: "do not allow directory traversal (backslash - windows separator)", givenPrefix: "/", givenRoot: "_fixture/", whenURL: `/..\\middleware/basic_auth.go`, expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, { name: "do not allow directory traversal (slash - unix separator)", givenPrefix: "/", givenRoot: "_fixture/", whenURL: `/../middleware/basic_auth.go`, expectStatus: http.StatusNotFound, expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Static(tc.givenPrefix, tc.givenRoot) req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectStatus, rec.Code) body := rec.Body.String() if tc.expectBodyStartsWith != "" { assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith)) } else { assert.Equal(t, "", body) } if tc.expectHeaderLocation != "" { assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0]) } else { _, ok := rec.Result().Header["Location"] assert.False(t, ok) } }) } } func TestEchoStaticRedirectIndex(t *testing.T) { e := New() // HandlerFunc e.Static("/static", "_fixture") errCh := make(chan error) go func() { errCh <- e.Start(":0") }() err := waitForServerStart(e, errCh, false) assert.NoError(t, err) addr := e.ListenerAddr().String() if resp, err := http.Get("http://" + addr + "/static"); err == nil { // http.Get follows redirects by default defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { assert.Fail(t, err.Error()) } }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) if body, err := io.ReadAll(resp.Body); err == nil { assert.Equal(t, true, strings.HasPrefix(string(body), "")) } else { assert.Fail(t, err.Error()) } } else { assert.NoError(t, err) } if err := e.Close(); err != nil { t.Fatal(err) } } func TestEchoFile(t *testing.T) { var testCases = []struct { name string givenPath string givenFile string whenPath string expectCode int expectStartsWith string }{ { name: "ok", givenPath: "/walle", givenFile: "_fixture/images/walle.png", whenPath: "/walle", expectCode: http.StatusOK, expectStartsWith: string([]byte{0x89, 0x50, 0x4e}), }, { name: "ok with relative path", givenPath: "/", givenFile: "./go.mod", whenPath: "/", expectCode: http.StatusOK, expectStartsWith: "module github.com/labstack/echo/v", }, { name: "nok file does not exist", givenPath: "/", givenFile: "./this-file-does-not-exist", whenPath: "/", expectCode: http.StatusNotFound, expectStartsWith: "{\"message\":\"Not Found\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() // we are using echo.defaultFS instance e.File(tc.givenPath, tc.givenFile) c, b := request(http.MethodGet, tc.whenPath, e) assert.Equal(t, tc.expectCode, c) if len(b) > len(tc.expectStartsWith) { b = b[:len(tc.expectStartsWith)] } assert.Equal(t, tc.expectStartsWith, b) }) } } func TestEchoMiddleware(t *testing.T) { e := New() buf := new(bytes.Buffer) e.Pre(func(next HandlerFunc) HandlerFunc { return func(c Context) error { assert.Empty(t, c.Path()) buf.WriteString("-1") return next(c) } }) e.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("1") return next(c) } }) e.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("2") return next(c) } }) e.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("3") return next(c) } }) // Route e.GET("/", func(c Context) error { return c.String(http.StatusOK, "OK") }) c, b := request(http.MethodGet, "/", e) assert.Equal(t, "-1123", buf.String()) assert.Equal(t, http.StatusOK, c) assert.Equal(t, "OK", b) } func TestEchoMiddlewareError(t *testing.T) { e := New() e.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { return errors.New("error") } }) e.GET("/", NotFoundHandler) c, _ := request(http.MethodGet, "/", e) assert.Equal(t, http.StatusInternalServerError, c) } func TestEchoHandler(t *testing.T) { e := New() // HandlerFunc e.GET("/ok", func(c Context) error { return c.String(http.StatusOK, "OK") }) c, b := request(http.MethodGet, "/ok", e) assert.Equal(t, http.StatusOK, c) assert.Equal(t, "OK", b) } func TestEchoWrapHandler(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, err := w.Write([]byte("test")) if err != nil { assert.Fail(t, err.Error()) } })) if assert.NoError(t, h(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "test", rec.Body.String()) } } func TestEchoWrapMiddleware(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) buf := new(bytes.Buffer) mw := WrapMiddleware(func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf.Write([]byte("mw")) h.ServeHTTP(w, r) }) }) h := mw(func(c Context) error { return c.String(http.StatusOK, "OK") }) if assert.NoError(t, h(c)) { assert.Equal(t, "mw", buf.String()) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "OK", rec.Body.String()) } } func TestEchoConnect(t *testing.T) { e := New() testMethod(t, http.MethodConnect, "/", e) } func TestEchoDelete(t *testing.T) { e := New() testMethod(t, http.MethodDelete, "/", e) } func TestEchoGet(t *testing.T) { e := New() testMethod(t, http.MethodGet, "/", e) } func TestEchoHead(t *testing.T) { e := New() testMethod(t, http.MethodHead, "/", e) } func TestEchoOptions(t *testing.T) { e := New() testMethod(t, http.MethodOptions, "/", e) } func TestEchoPatch(t *testing.T) { e := New() testMethod(t, http.MethodPatch, "/", e) } func TestEchoPost(t *testing.T) { e := New() testMethod(t, http.MethodPost, "/", e) } func TestEchoPut(t *testing.T) { e := New() testMethod(t, http.MethodPut, "/", e) } func TestEchoTrace(t *testing.T) { e := New() testMethod(t, http.MethodTrace, "/", e) } func TestEchoAny(t *testing.T) { // JFC e := New() e.Any("/", func(c Context) error { return c.String(http.StatusOK, "Any") }) } func TestEchoMatch(t *testing.T) { // JFC e := New() e.Match([]string{http.MethodGet, http.MethodPost}, "/", func(c Context) error { return c.String(http.StatusOK, "Match") }) } func TestEchoURL(t *testing.T) { e := New() static := func(Context) error { return nil } getUser := func(Context) error { return nil } getAny := func(Context) error { return nil } getFile := func(Context) error { return nil } e.GET("/static/file", static) e.GET("/users/:id", getUser) e.GET("/documents/*", getAny) g := e.Group("/group") g.GET("/users/:uid/files/:fid", getFile) assert.Equal(t, "/static/file", e.URL(static)) assert.Equal(t, "/users/:id", e.URL(getUser)) assert.Equal(t, "/users/1", e.URL(getUser, "1")) assert.Equal(t, "/users/1", e.URL(getUser, "1")) assert.Equal(t, "/documents/foo.txt", e.URL(getAny, "foo.txt")) assert.Equal(t, "/documents/*", e.URL(getAny)) assert.Equal(t, "/group/users/1/files/:fid", e.URL(getFile, "1")) assert.Equal(t, "/group/users/1/files/1", e.URL(getFile, "1", "1")) } func TestEchoRoutes(t *testing.T) { e := New() routes := []*Route{ {http.MethodGet, "/users/:user/events", ""}, {http.MethodGet, "/users/:user/events/public", ""}, {http.MethodPost, "/repos/:owner/:repo/git/refs", ""}, {http.MethodPost, "/repos/:owner/:repo/git/tags", ""}, } for _, r := range routes { e.Add(r.Method, r.Path, func(c Context) error { return c.String(http.StatusOK, "OK") }) } if assert.Equal(t, len(routes), len(e.Routes())) { for _, r := range e.Routes() { found := false for _, rr := range routes { if r.Method == rr.Method && r.Path == rr.Path { found = true break } } if !found { t.Errorf("Route %s %s not found", r.Method, r.Path) } } } } func TestEchoRoutesHandleAdditionalHosts(t *testing.T) { e := New() domain2Router := e.Host("domain2.router.com") routes := []*Route{ {http.MethodGet, "/users/:user/events", ""}, {http.MethodGet, "/users/:user/events/public", ""}, {http.MethodPost, "/repos/:owner/:repo/git/refs", ""}, {http.MethodPost, "/repos/:owner/:repo/git/tags", ""}, } for _, r := range routes { domain2Router.Add(r.Method, r.Path, func(c Context) error { return c.String(http.StatusOK, "OK") }) } e.Add(http.MethodGet, "/api", func(c Context) error { return c.String(http.StatusOK, "OK") }) domain2Routes := e.Routers()["domain2.router.com"].Routes() assert.Len(t, domain2Routes, len(routes)) for _, r := range domain2Routes { found := false for _, rr := range routes { if r.Method == rr.Method && r.Path == rr.Path { found = true break } } if !found { t.Errorf("Route %s %s not found", r.Method, r.Path) } } } func TestEchoRoutesHandleDefaultHost(t *testing.T) { e := New() routes := []*Route{ {http.MethodGet, "/users/:user/events", ""}, {http.MethodGet, "/users/:user/events/public", ""}, {http.MethodPost, "/repos/:owner/:repo/git/refs", ""}, {http.MethodPost, "/repos/:owner/:repo/git/tags", ""}, } for _, r := range routes { e.Add(r.Method, r.Path, func(c Context) error { return c.String(http.StatusOK, "OK") }) } e.Host("subdomain.mysite.site").Add(http.MethodGet, "/api", func(c Context) error { return c.String(http.StatusOK, "OK") }) defaultRouterRoutes := e.Routes() assert.Len(t, defaultRouterRoutes, len(routes)) for _, r := range defaultRouterRoutes { found := false for _, rr := range routes { if r.Method == rr.Method && r.Path == rr.Path { found = true break } } if !found { t.Errorf("Route %s %s not found", r.Method, r.Path) } } } func TestEchoServeHTTPPathEncoding(t *testing.T) { e := New() e.GET("/with/slash", func(c Context) error { return c.String(http.StatusOK, "/with/slash") }) e.GET("/:id", func(c Context) error { return c.String(http.StatusOK, c.Param("id")) }) var testCases = []struct { name string whenURL string expectURL string expectStatus int }{ { name: "url with encoding is not decoded for routing", whenURL: "/with%2Fslash", expectURL: "with%2Fslash", // `%2F` is not decoded to `/` for routing expectStatus: http.StatusOK, }, { name: "url without encoding is used as is", whenURL: "/with/slash", expectURL: "/with/slash", expectStatus: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectStatus, rec.Code) assert.Equal(t, tc.expectURL, rec.Body.String()) }) } } func TestEchoHost(t *testing.T) { okHandler := func(c Context) error { return c.String(http.StatusOK, http.StatusText(http.StatusOK)) } teapotHandler := func(c Context) error { return c.String(http.StatusTeapot, http.StatusText(http.StatusTeapot)) } acceptHandler := func(c Context) error { return c.String(http.StatusAccepted, http.StatusText(http.StatusAccepted)) } teapotMiddleware := MiddlewareFunc(func(next HandlerFunc) HandlerFunc { return teapotHandler }) e := New() e.GET("/", acceptHandler) e.GET("/foo", acceptHandler) ok := e.Host("ok.com") ok.GET("/", okHandler) ok.GET("/foo", okHandler) teapot := e.Host("teapot.com") teapot.GET("/", teapotHandler) teapot.GET("/foo", teapotHandler) middle := e.Host("middleware.com", teapotMiddleware) middle.GET("/", okHandler) middle.GET("/foo", okHandler) var testCases = []struct { name string whenHost string whenPath string expectBody string expectStatus int }{ { name: "No Host Root", whenHost: "", whenPath: "/", expectBody: http.StatusText(http.StatusAccepted), expectStatus: http.StatusAccepted, }, { name: "No Host Foo", whenHost: "", whenPath: "/foo", expectBody: http.StatusText(http.StatusAccepted), expectStatus: http.StatusAccepted, }, { name: "OK Host Root", whenHost: "ok.com", whenPath: "/", expectBody: http.StatusText(http.StatusOK), expectStatus: http.StatusOK, }, { name: "OK Host Foo", whenHost: "ok.com", whenPath: "/foo", expectBody: http.StatusText(http.StatusOK), expectStatus: http.StatusOK, }, { name: "Teapot Host Root", whenHost: "teapot.com", whenPath: "/", expectBody: http.StatusText(http.StatusTeapot), expectStatus: http.StatusTeapot, }, { name: "Teapot Host Foo", whenHost: "teapot.com", whenPath: "/foo", expectBody: http.StatusText(http.StatusTeapot), expectStatus: http.StatusTeapot, }, { name: "Middleware Host", whenHost: "middleware.com", whenPath: "/", expectBody: http.StatusText(http.StatusTeapot), expectStatus: http.StatusTeapot, }, { name: "Middleware Host Foo", whenHost: "middleware.com", whenPath: "/foo", expectBody: http.StatusText(http.StatusTeapot), expectStatus: http.StatusTeapot, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.whenPath, nil) req.Host = tc.whenHost rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectStatus, rec.Code) assert.Equal(t, tc.expectBody, rec.Body.String()) }) } } func TestEchoGroup(t *testing.T) { e := New() buf := new(bytes.Buffer) e.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("0") return next(c) } })) h := func(c Context) error { return c.NoContent(http.StatusOK) } //-------- // Routes //-------- e.GET("/users", h) // Group g1 := e.Group("/group1") g1.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("1") return next(c) } }) g1.GET("", h) // Nested groups with middleware g2 := e.Group("/group2") g2.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("2") return next(c) } }) g3 := g2.Group("/group3") g3.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { buf.WriteString("3") return next(c) } }) g3.GET("", h) request(http.MethodGet, "/users", e) assert.Equal(t, "0", buf.String()) buf.Reset() request(http.MethodGet, "/group1", e) assert.Equal(t, "01", buf.String()) buf.Reset() request(http.MethodGet, "/group2/group3", e) assert.Equal(t, "023", buf.String()) } func TestEchoNotFound(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/files", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestEcho_RouteNotFound(t *testing.T) { var testCases = []struct { name string whenURL string expectRoute interface{} expectCode int }{ { name: "404, route to static not found handler /a/c/xx", whenURL: "/a/c/xx", expectRoute: "GET /a/c/xx", expectCode: http.StatusNotFound, }, { name: "404, route to path param not found handler /a/:file", whenURL: "/a/echo.exe", expectRoute: "GET /a/:file", expectCode: http.StatusNotFound, }, { name: "404, route to any not found handler /*", whenURL: "/b/echo.exe", expectRoute: "GET /*", expectCode: http.StatusNotFound, }, { name: "200, route /a/c/df to /a/c/df", whenURL: "/a/c/df", expectRoute: "GET /a/c/df", expectCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() okHandler := func(c Context) error { return c.String(http.StatusOK, c.Request().Method+" "+c.Path()) } notFoundHandler := func(c Context) error { return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path()) } e.GET("/", okHandler) e.GET("/a/c/df", okHandler) e.GET("/a/b*", okHandler) e.PUT("/*", okHandler) e.RouteNotFound("/a/c/xx", notFoundHandler) // static e.RouteNotFound("/a/:file", notFoundHandler) // param e.RouteNotFound("/*", notFoundHandler) // any req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectCode, rec.Code) assert.Equal(t, tc.expectRoute, rec.Body.String()) }) } } func TestEchoMethodNotAllowed(t *testing.T) { e := New() e.GET("/", func(c Context) error { return c.String(http.StatusOK, "Echo!") }) req := httptest.NewRequest(http.MethodPost, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow)) } func TestEchoContext(t *testing.T) { e := New() c := e.AcquireContext() assert.IsType(t, new(context), c) e.ReleaseContext(c) } func waitForServerStart(e *Echo, errChan <-chan error, isTLS bool) error { ctx, cancel := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond) defer cancel() ticker := time.NewTicker(5 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: var addr net.Addr if isTLS { addr = e.TLSListenerAddr() } else { addr = e.ListenerAddr() } if addr != nil && strings.Contains(addr.String(), ":") { return nil // was started } case err := <-errChan: if err == http.ErrServerClosed { return nil } return err } } } func TestEchoStart(t *testing.T) { e := New() errChan := make(chan error) go func() { err := e.Start(":0") if err != nil { errChan <- err } }() err := waitForServerStart(e, errChan, false) assert.NoError(t, err) assert.NoError(t, e.Close()) } func TestEcho_StartTLS(t *testing.T) { var testCases = []struct { name string addr string certFile string keyFile string expectError string }{ { name: "ok", addr: ":0", }, { name: "nok, invalid certFile", addr: ":0", certFile: "not existing", expectError: "open not existing: no such file or directory", }, { name: "nok, invalid keyFile", addr: ":0", keyFile: "not existing", expectError: "open not existing: no such file or directory", }, { name: "nok, failed to create cert out of certFile and keyFile", addr: ":0", keyFile: "_fixture/certs/cert.pem", // we are passing cert instead of key expectError: "tls: found a certificate rather than a key in the PEM for the private key", }, { name: "nok, invalid tls address", addr: "nope", expectError: "listen tcp: address nope: missing port in address", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() errChan := make(chan error) go func() { certFile := "_fixture/certs/cert.pem" if tc.certFile != "" { certFile = tc.certFile } keyFile := "_fixture/certs/key.pem" if tc.keyFile != "" { keyFile = tc.keyFile } err := e.StartTLS(tc.addr, certFile, keyFile) if err != nil { errChan <- err } }() err := waitForServerStart(e, errChan, true) if tc.expectError != "" { if _, ok := err.(*os.PathError); ok { assert.Error(t, err) // error messages for unix and windows are different. so test only error type here } else { assert.EqualError(t, err, tc.expectError) } } else { assert.NoError(t, err) } assert.NoError(t, e.Close()) }) } } func TestEchoStartTLSAndStart(t *testing.T) { // We test if Echo and listeners work correctly when Echo is simultaneously attached to HTTP and HTTPS server e := New() e.GET("/", func(c Context) error { return c.String(http.StatusOK, "OK") }) errTLSChan := make(chan error) go func() { certFile := "_fixture/certs/cert.pem" keyFile := "_fixture/certs/key.pem" err := e.StartTLS("localhost:", certFile, keyFile) if err != nil { errTLSChan <- err } }() err := waitForServerStart(e, errTLSChan, true) assert.NoError(t, err) defer func() { if err := e.Shutdown(stdContext.Background()); err != nil { t.Error(err) } }() // check if HTTPS works (note: we are using self signed certs so InsecureSkipVerify=true) client := &http.Client{Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }} res, err := client.Get("https://" + e.TLSListenerAddr().String()) assert.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) errChan := make(chan error) go func() { err := e.Start("localhost:") if err != nil { errChan <- err } }() err = waitForServerStart(e, errChan, false) assert.NoError(t, err) // now we are serving both HTTPS and HTTP listeners. see if HTTP works in addition to HTTPS res, err = http.Get("http://" + e.ListenerAddr().String()) assert.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) // see if HTTPS works after HTTP listener is also added res, err = client.Get("https://" + e.TLSListenerAddr().String()) assert.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) } func TestEchoStartTLSByteString(t *testing.T) { cert, err := os.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := os.ReadFile("_fixture/certs/key.pem") require.NoError(t, err) testCases := []struct { cert interface{} key interface{} expectedErr error name string }{ { cert: "_fixture/certs/cert.pem", key: "_fixture/certs/key.pem", expectedErr: nil, name: `ValidCertAndKeyFilePath`, }, { cert: cert, key: key, expectedErr: nil, name: `ValidCertAndKeyByteString`, }, { cert: cert, key: 1, expectedErr: ErrInvalidCertOrKeyType, name: `InvalidKeyType`, }, { cert: 0, key: key, expectedErr: ErrInvalidCertOrKeyType, name: `InvalidCertType`, }, { cert: 0, key: 1, expectedErr: ErrInvalidCertOrKeyType, name: `InvalidCertAndKeyTypes`, }, } for _, test := range testCases { test := test t.Run(test.name, func(t *testing.T) { e := New() e.HideBanner = true errChan := make(chan error) go func() { errChan <- e.StartTLS(":0", test.cert, test.key) }() err := waitForServerStart(e, errChan, true) if test.expectedErr != nil { assert.EqualError(t, err, test.expectedErr.Error()) } else { assert.NoError(t, err) } assert.NoError(t, e.Close()) }) } } func TestEcho_StartAutoTLS(t *testing.T) { var testCases = []struct { name string addr string expectError string }{ { name: "ok", addr: ":0", }, { name: "nok, invalid address", addr: "nope", expectError: "listen tcp: address nope: missing port in address", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() errChan := make(chan error) go func() { errChan <- e.StartAutoTLS(tc.addr) }() err := waitForServerStart(e, errChan, true) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.NoError(t, e.Close()) }) } } func TestEcho_StartH2CServer(t *testing.T) { var testCases = []struct { name string addr string expectError string }{ { name: "ok", addr: ":0", }, { name: "nok, invalid address", addr: "nope", expectError: "listen tcp: address nope: missing port in address", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Debug = true h2s := &http2.Server{} errChan := make(chan error) go func() { err := e.StartH2CServer(tc.addr, h2s) if err != nil { errChan <- err } }() err := waitForServerStart(e, errChan, false) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.NoError(t, e.Close()) }) } } func testMethod(t *testing.T, method, path string, e *Echo) { p := reflect.ValueOf(path) h := reflect.ValueOf(func(c Context) error { return c.String(http.StatusOK, method) }) i := interface{}(e) reflect.ValueOf(i).MethodByName(method).Call([]reflect.Value{p, h}) _, body := request(method, path, e) assert.Equal(t, method, body) } func request(method, path string, e *Echo) (int, string) { req := httptest.NewRequest(method, path, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) return rec.Code, rec.Body.String() } func TestHTTPError(t *testing.T) { t.Run("non-internal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) assert.Equal(t, "code=400, message=map[code:12]", err.Error()) }) t.Run("internal and SetInternal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) err.SetInternal(errors.New("internal error")) assert.Equal(t, "code=400, message=map[code:12], internal=internal error", err.Error()) }) t.Run("internal and WithInternal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) err = err.WithInternal(errors.New("internal error")) assert.Equal(t, "code=400, message=map[code:12], internal=internal error", err.Error()) }) } func TestHTTPError_Unwrap(t *testing.T) { t.Run("non-internal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) assert.Nil(t, errors.Unwrap(err)) }) t.Run("unwrap internal and SetInternal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) err.SetInternal(errors.New("internal error")) assert.Equal(t, "internal error", errors.Unwrap(err).Error()) }) t.Run("unwrap internal and WithInternal", func(t *testing.T) { err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{ "code": 12, }) err = err.WithInternal(errors.New("internal error")) assert.Equal(t, "internal error", errors.Unwrap(err).Error()) }) } type customError struct { s string } func (ce *customError) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`{"x":"%v"}`, ce.s)), nil } func (ce *customError) Error() string { return ce.s } func TestDefaultHTTPErrorHandler(t *testing.T) { var testCases = []struct { name string givenDebug bool whenPath string expectCode int expectBody string }{ { name: "with Debug=true plain response contains error message", givenDebug: true, whenPath: "/plain", expectCode: http.StatusInternalServerError, expectBody: "{\n \"error\": \"an error occurred\",\n \"message\": \"Internal Server Error\"\n}\n", }, { name: "with Debug=true special handling for HTTPError", givenDebug: true, whenPath: "/badrequest", expectCode: http.StatusBadRequest, expectBody: "{\n \"error\": \"code=400, message=Invalid request\",\n \"message\": \"Invalid request\"\n}\n", }, { name: "with Debug=true complex errors are serialized to pretty JSON", givenDebug: true, whenPath: "/servererror", expectCode: http.StatusInternalServerError, expectBody: "{\n \"code\": 33,\n \"error\": \"stackinfo\",\n \"message\": \"Something bad happened\"\n}\n", }, { name: "with Debug=true if the body is already set HTTPErrorHandler should not add anything to response body", givenDebug: true, whenPath: "/early-return", expectCode: http.StatusOK, expectBody: "OK", }, { name: "with Debug=true internal error should be reflected in the message", givenDebug: true, whenPath: "/internal-error", expectCode: http.StatusBadRequest, expectBody: "{\n \"error\": \"code=400, message=Bad Request, internal=internal error message body\",\n \"message\": \"Bad Request\"\n}\n", }, { name: "with Debug=false the error response is shortened", whenPath: "/plain", expectCode: http.StatusInternalServerError, expectBody: "{\"message\":\"Internal Server Error\"}\n", }, { name: "with Debug=false the error response is shortened", whenPath: "/badrequest", expectCode: http.StatusBadRequest, expectBody: "{\"message\":\"Invalid request\"}\n", }, { name: "with Debug=false No difference for error response with non plain string errors", whenPath: "/servererror", expectCode: http.StatusInternalServerError, expectBody: "{\"code\":33,\"error\":\"stackinfo\",\"message\":\"Something bad happened\"}\n", }, { name: "with Debug=false when httpError contains an error", whenPath: "/error-in-httperror", expectCode: http.StatusBadRequest, expectBody: "{\"message\":\"error in httperror\"}\n", }, { name: "with Debug=false when httpError contains an error", whenPath: "/customerror-in-httperror", expectCode: http.StatusBadRequest, expectBody: "{\"x\":\"custom error msg\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Debug = tc.givenDebug // With Debug=true plain response contains error message e.Any("/plain", func(c Context) error { return errors.New("an error occurred") }) e.Any("/badrequest", func(c Context) error { // and special handling for HTTPError return NewHTTPError(http.StatusBadRequest, "Invalid request") }) e.Any("/servererror", func(c Context) error { // complex errors are serialized to pretty JSON return NewHTTPError(http.StatusInternalServerError, map[string]interface{}{ "code": 33, "message": "Something bad happened", "error": "stackinfo", }) }) // if the body is already set HTTPErrorHandler should not add anything to response body e.Any("/early-return", func(c Context) error { err := c.String(http.StatusOK, "OK") if err != nil { assert.Fail(t, err.Error()) } return errors.New("ERROR") }) // internal error should be reflected in the message e.GET("/internal-error", func(c Context) error { err := errors.New("internal error message body") return NewHTTPError(http.StatusBadRequest).SetInternal(err) }) e.GET("/error-in-httperror", func(c Context) error { return NewHTTPError(http.StatusBadRequest, errors.New("error in httperror")) }) e.GET("/customerror-in-httperror", func(c Context) error { return NewHTTPError(http.StatusBadRequest, &customError{s: "custom error msg"}) }) c, b := request(http.MethodGet, tc.whenPath, e) assert.Equal(t, tc.expectCode, c) assert.Equal(t, tc.expectBody, b) }) } } func TestEchoClose(t *testing.T) { e := New() errCh := make(chan error) go func() { errCh <- e.Start(":0") }() err := waitForServerStart(e, errCh, false) assert.NoError(t, err) if err := e.Close(); err != nil { t.Fatal(err) } assert.NoError(t, e.Close()) err = <-errCh assert.Equal(t, err.Error(), "http: Server closed") } func TestEchoShutdown(t *testing.T) { e := New() errCh := make(chan error) go func() { errCh <- e.Start(":0") }() err := waitForServerStart(e, errCh, false) assert.NoError(t, err) if err := e.Close(); err != nil { t.Fatal(err) } ctx, cancel := stdContext.WithTimeout(stdContext.Background(), 10*time.Second) defer cancel() assert.NoError(t, e.Shutdown(ctx)) err = <-errCh assert.Equal(t, err.Error(), "http: Server closed") } var listenerNetworkTests = []struct { test string network string address string }{ {"tcp ipv4 address", "tcp", "127.0.0.1:1323"}, {"tcp ipv6 address", "tcp", "[::1]:1323"}, {"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"}, {"tcp6 ipv6 address", "tcp6", "[::1]:1323"}, } func supportsIPv6() bool { addrs, _ := net.InterfaceAddrs() for _, addr := range addrs { // Check if any interface has local IPv6 assigned if strings.Contains(addr.String(), "::1") { return true } } return false } func TestEchoListenerNetwork(t *testing.T) { hasIPv6 := supportsIPv6() for _, tt := range listenerNetworkTests { if !hasIPv6 && strings.Contains(tt.address, "::") { t.Skip("Skipping testing IPv6 for " + tt.address + ", not available") continue } t.Run(tt.test, func(t *testing.T) { e := New() e.ListenerNetwork = tt.network // HandlerFunc e.GET("/ok", func(c Context) error { return c.String(http.StatusOK, "OK") }) errCh := make(chan error) go func() { errCh <- e.Start(tt.address) }() err := waitForServerStart(e, errCh, false) assert.NoError(t, err) if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil { defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { assert.Fail(t, err.Error()) } }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) if body, err := io.ReadAll(resp.Body); err == nil { assert.Equal(t, "OK", string(body)) } else { assert.Fail(t, err.Error()) } } else { assert.Fail(t, err.Error()) } if err := e.Close(); err != nil { t.Fatal(err) } }) } } func TestEchoListenerNetworkInvalid(t *testing.T) { e := New() e.ListenerNetwork = "unix" // HandlerFunc e.GET("/ok", func(c Context) error { return c.String(http.StatusOK, "OK") }) assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323")) } func TestEcho_OnAddRouteHandler(t *testing.T) { type rr struct { host string route Route handler HandlerFunc middleware []MiddlewareFunc } dummyHandler := func(Context) error { return nil } e := New() added := make([]rr, 0) e.OnAddRouteHandler = func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc) { added = append(added, rr{ host: host, route: route, handler: handler, middleware: middleware, }) } e.GET("/static", NotFoundHandler) e.Host("domain.site").GET("/static/*", dummyHandler, func(next HandlerFunc) HandlerFunc { return func(c Context) error { return next(c) } }) assert.Len(t, added, 2) assert.Equal(t, "", added[0].host) assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.glob..func1"}, added[0].route) assert.Len(t, added[0].middleware, 0) assert.Equal(t, "domain.site", added[1].host) assert.Equal(t, Route{Method: http.MethodGet, Path: "/static/*", Name: "github.com/labstack/echo/v4.TestEcho_OnAddRouteHandler.func1"}, added[1].route) assert.Len(t, added[1].middleware, 1) } func TestEchoReverse(t *testing.T) { var testCases = []struct { name string whenRouteName string whenParams []interface{} expect string }{ { name: "ok,static with no params", whenRouteName: "/static", expect: "/static", }, { name: "ok,static with non existent param", whenRouteName: "/static", whenParams: []interface{}{"missing param"}, expect: "/static", }, { name: "ok, wildcard with no params", whenRouteName: "/static/*", expect: "/static/*", }, { name: "ok, wildcard with params", whenRouteName: "/static/*", whenParams: []interface{}{"foo.txt"}, expect: "/static/foo.txt", }, { name: "ok, single param without param", whenRouteName: "/params/:foo", expect: "/params/:foo", }, { name: "ok, single param with param", whenRouteName: "/params/:foo", whenParams: []interface{}{"one"}, expect: "/params/one", }, { name: "ok, multi param without params", whenRouteName: "/params/:foo/bar/:qux", expect: "/params/:foo/bar/:qux", }, { name: "ok, multi param with one param", whenRouteName: "/params/:foo/bar/:qux", whenParams: []interface{}{"one"}, expect: "/params/one/bar/:qux", }, { name: "ok, multi param with all params", whenRouteName: "/params/:foo/bar/:qux", whenParams: []interface{}{"one", "two"}, expect: "/params/one/bar/two", }, { name: "ok, multi param + wildcard with all params", whenRouteName: "/params/:foo/bar/:qux/*", whenParams: []interface{}{"one", "two", "three"}, expect: "/params/one/bar/two/three", }, { name: "ok, backslash is not escaped", whenRouteName: "/backslash", whenParams: []interface{}{"test"}, expect: `/a\b/test`, }, { name: "ok, escaped colon verbs", whenRouteName: "/params:customVerb", whenParams: []interface{}{"PATCH"}, expect: `/params:PATCH`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() dummyHandler := func(Context) error { return nil } e.GET("/static", dummyHandler).Name = "/static" e.GET("/static/*", dummyHandler).Name = "/static/*" e.GET("/params/:foo", dummyHandler).Name = "/params/:foo" e.GET("/params/:foo/bar/:qux", dummyHandler).Name = "/params/:foo/bar/:qux" e.GET("/params/:foo/bar/:qux/*", dummyHandler).Name = "/params/:foo/bar/:qux/*" e.GET("/a\\b/:x", dummyHandler).Name = "/backslash" e.GET("/params\\::customVerb", dummyHandler).Name = "/params:customVerb" assert.Equal(t, tc.expect, e.Reverse(tc.whenRouteName, tc.whenParams...)) }) } } func TestEchoReverseHandleHostProperly(t *testing.T) { dummyHandler := func(Context) error { return nil } e := New() // routes added to the default router are different form different hosts e.GET("/static", dummyHandler).Name = "default-host /static" e.GET("/static/*", dummyHandler).Name = "xxx" // different host h := e.Host("the_host") h.GET("/static", dummyHandler).Name = "host2 /static" h.GET("/static/v2/*", dummyHandler).Name = "xxx" assert.Equal(t, "/static", e.Reverse("default-host /static")) // when actual route does not have params and we provide some to Reverse we should get that route url back assert.Equal(t, "/static", e.Reverse("default-host /static", "missing param")) host2Router := e.Routers()["the_host"] assert.Equal(t, "/static", host2Router.Reverse("host2 /static")) assert.Equal(t, "/static", host2Router.Reverse("host2 /static", "missing param")) assert.Equal(t, "/static/v2/*", host2Router.Reverse("xxx")) assert.Equal(t, "/static/v2/foo.txt", host2Router.Reverse("xxx", "foo.txt")) } func TestEcho_ListenerAddr(t *testing.T) { e := New() addr := e.ListenerAddr() assert.Nil(t, addr) errCh := make(chan error) go func() { errCh <- e.Start(":0") }() err := waitForServerStart(e, errCh, false) assert.NoError(t, err) } func TestEcho_TLSListenerAddr(t *testing.T) { cert, err := os.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := os.ReadFile("_fixture/certs/key.pem") require.NoError(t, err) e := New() addr := e.TLSListenerAddr() assert.Nil(t, addr) errCh := make(chan error) go func() { errCh <- e.StartTLS(":0", cert, key) }() err = waitForServerStart(e, errCh, true) assert.NoError(t, err) } func TestEcho_StartServer(t *testing.T) { cert, err := os.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := os.ReadFile("_fixture/certs/key.pem") require.NoError(t, err) certs, err := tls.X509KeyPair(cert, key) require.NoError(t, err) var testCases = []struct { name string addr string TLSConfig *tls.Config expectError string }{ { name: "ok", addr: ":0", }, { name: "ok, start with TLS", addr: ":0", TLSConfig: &tls.Config{Certificates: []tls.Certificate{certs}}, }, { name: "nok, invalid address", addr: "nope", expectError: "listen tcp: address nope: missing port in address", }, { name: "nok, invalid tls address", addr: "nope", TLSConfig: &tls.Config{InsecureSkipVerify: true}, expectError: "listen tcp: address nope: missing port in address", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Debug = true server := new(http.Server) server.Addr = tc.addr if tc.TLSConfig != nil { server.TLSConfig = tc.TLSConfig } errCh := make(chan error) go func() { errCh <- e.StartServer(server) }() err := waitForServerStart(e, errCh, tc.TLSConfig != nil) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.NoError(t, e.Close()) }) } } func benchmarkEchoRoutes(b *testing.B, routes []*Route) { e := New() req := httptest.NewRequest("GET", "/", nil) u := req.URL w := httptest.NewRecorder() b.ReportAllocs() // Add routes for _, route := range routes { e.Add(route.Method, route.Path, func(c Context) error { return nil }) } // Find routes b.ResetTimer() for i := 0; i < b.N; i++ { for _, route := range routes { req.Method = route.Method u.Path = route.Path e.ServeHTTP(w, req) } } } func BenchmarkEchoStaticRoutes(b *testing.B) { benchmarkEchoRoutes(b, staticRoutes) } func BenchmarkEchoStaticRoutesMisses(b *testing.B) { benchmarkEchoRoutes(b, staticRoutes) } func BenchmarkEchoGitHubAPI(b *testing.B) { benchmarkEchoRoutes(b, gitHubAPI) } func BenchmarkEchoGitHubAPIMisses(b *testing.B) { benchmarkEchoRoutes(b, gitHubAPI) } func BenchmarkEchoParseAPI(b *testing.B) { benchmarkEchoRoutes(b, parseAPI) } golang-github-labstack-echo-4.11.1/go.mod000066400000000000000000000012411445647333300201430ustar00rootroot00000000000000module github.com/labstack/echo/v4 go 1.17 require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/labstack/gommon v0.4.0 github.com/stretchr/testify v1.8.1 github.com/valyala/fasttemplate v1.2.2 golang.org/x/crypto v0.11.0 golang.org/x/net v0.12.0 golang.org/x/time v0.3.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-labstack-echo-4.11.1/go.sum000066400000000000000000000174011445647333300201750ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-labstack-echo-4.11.1/group.go000066400000000000000000000114441445647333300205260ustar00rootroot00000000000000package echo import ( "net/http" ) type ( // Group is a set of sub-routes for a specified route. It can be used for inner // routes that share a common middleware or functionality that should be separate // from the parent echo instance while still inheriting from it. Group struct { common host string prefix string middleware []MiddlewareFunc echo *Echo } ) // Use implements `Echo#Use()` for sub-routes within the Group. func (g *Group) Use(middleware ...MiddlewareFunc) { g.middleware = append(g.middleware, middleware...) if len(g.middleware) == 0 { return } // group level middlewares are different from Echo `Pre` and `Use` middlewares (those are global). Group level middlewares // are only executed if they are added to the Router with route. // So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the // Router would find route to match our request path and therefore guarantee the middleware(s) will get executed. g.RouteNotFound("", NotFoundHandler) g.RouteNotFound("/*", NotFoundHandler) } // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodConnect, path, h, m...) } // DELETE implements `Echo#DELETE()` for sub-routes within the Group. func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodDelete, path, h, m...) } // GET implements `Echo#GET()` for sub-routes within the Group. func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodGet, path, h, m...) } // HEAD implements `Echo#HEAD()` for sub-routes within the Group. func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodHead, path, h, m...) } // OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodOptions, path, h, m...) } // PATCH implements `Echo#PATCH()` for sub-routes within the Group. func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodPatch, path, h, m...) } // POST implements `Echo#POST()` for sub-routes within the Group. func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodPost, path, h, m...) } // PUT implements `Echo#PUT()` for sub-routes within the Group. func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodPut, path, h, m...) } // TRACE implements `Echo#TRACE()` for sub-routes within the Group. func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(http.MethodTrace, path, h, m...) } // Any implements `Echo#Any()` for sub-routes within the Group. func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { routes := make([]*Route, len(methods)) for i, m := range methods { routes[i] = g.Add(m, path, handler, middleware...) } return routes } // Match implements `Echo#Match()` for sub-routes within the Group. func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { routes := make([]*Route, len(methods)) for i, m := range methods { routes[i] = g.Add(m, path, handler, middleware...) } return routes } // Group creates a new sub-group with prefix and optional sub-group-level middleware. func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) (sg *Group) { m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware)) m = append(m, g.middleware...) m = append(m, middleware...) sg = g.echo.Group(g.prefix+prefix, m...) sg.host = g.host return } // File implements `Echo#File()` for sub-routes within the Group. func (g *Group) File(path, file string) { g.file(path, file, g.GET) } // RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group. // // Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` func (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { return g.Add(RouteNotFound, path, h, m...) } // Add implements `Echo#Add()` for sub-routes within the Group. func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route { // Combine into a new slice to avoid accidentally passing the same slice for // multiple routes, which would lead to later add() calls overwriting the // middleware from earlier calls. m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware)) m = append(m, g.middleware...) m = append(m, middleware...) return g.echo.add(g.host, method, g.prefix+path, handler, m...) } golang-github-labstack-echo-4.11.1/group_fs.go000066400000000000000000000017201445647333300212120ustar00rootroot00000000000000package echo import ( "io/fs" "net/http" ) // Static implements `Echo#Static()` for sub-routes within the Group. func (g *Group) Static(pathPrefix, fsRoot string) { subFs := MustSubFS(g.echo.Filesystem, fsRoot) g.StaticFS(pathPrefix, subFs) } // StaticFS implements `Echo#StaticFS()` for sub-routes within the Group. // // When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary // prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths // including `assets/images` as their prefix. func (g *Group) StaticFS(pathPrefix string, filesystem fs.FS) { g.Add( http.MethodGet, pathPrefix+"*", StaticDirectoryHandler(filesystem, false), ) } // FileFS implements `Echo#FileFS()` for sub-routes within the Group. func (g *Group) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route { return g.GET(path, StaticFileHandler(file, filesystem), m...) } golang-github-labstack-echo-4.11.1/group_fs_test.go000066400000000000000000000047461445647333300222640ustar00rootroot00000000000000package echo import ( "github.com/stretchr/testify/assert" "io/fs" "net/http" "net/http/httptest" "os" "testing" ) func TestGroup_FileFS(t *testing.T) { var testCases = []struct { name string whenPath string whenFile string whenFS fs.FS givenURL string expectCode int expectStartsWith []byte }{ { name: "ok", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "walle.png", givenURL: "/assets/walle", expectCode: http.StatusOK, expectStartsWith: []byte{0x89, 0x50, 0x4e}, }, { name: "nok, requesting invalid path", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "walle.png", givenURL: "/assets/walle.png", expectCode: http.StatusNotFound, expectStartsWith: []byte(`{"message":"Not Found"}`), }, { name: "nok, serving not existent file from filesystem", whenPath: "/walle", whenFS: os.DirFS("_fixture/images"), whenFile: "not-existent.png", givenURL: "/assets/walle", expectCode: http.StatusNotFound, expectStartsWith: []byte(`{"message":"Not Found"}`), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() g := e.Group("/assets") g.FileFS(tc.whenPath, tc.whenFile, tc.whenFS) req := httptest.NewRequest(http.MethodGet, tc.givenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectCode, rec.Code) body := rec.Body.Bytes() if len(body) > len(tc.expectStartsWith) { body = body[:len(tc.expectStartsWith)] } assert.Equal(t, tc.expectStartsWith, body) }) } } func TestGroup_StaticPanic(t *testing.T) { var testCases = []struct { name string givenRoot string expectError string }{ { name: "panics for ../", givenRoot: "../images", expectError: "can not create sub FS, invalid root given, err: sub ../images: invalid name", }, { name: "panics for /", givenRoot: "/images", expectError: "can not create sub FS, invalid root given, err: sub /images: invalid name", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() e.Filesystem = os.DirFS("./") g := e.Group("/assets") assert.PanicsWithError(t, tc.expectError, func() { g.Static("/images", tc.givenRoot) }) }) } } golang-github-labstack-echo-4.11.1/group_test.go000066400000000000000000000146411445647333300215670ustar00rootroot00000000000000package echo import ( "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" ) // TODO: Fix me func TestGroup(t *testing.T) { g := New().Group("/group") h := func(Context) error { return nil } g.CONNECT("/", h) g.DELETE("/", h) g.GET("/", h) g.HEAD("/", h) g.OPTIONS("/", h) g.PATCH("/", h) g.POST("/", h) g.PUT("/", h) g.TRACE("/", h) g.Any("/", h) g.Match([]string{http.MethodGet, http.MethodPost}, "/", h) g.Static("/static", "/tmp") g.File("/walle", "_fixture/images//walle.png") } func TestGroupFile(t *testing.T) { e := New() g := e.Group("/group") g.File("/walle", "_fixture/images/walle.png") expectedData, err := os.ReadFile("_fixture/images/walle.png") assert.Nil(t, err) req := httptest.NewRequest(http.MethodGet, "/group/walle", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, expectedData, rec.Body.Bytes()) } func TestGroupRouteMiddleware(t *testing.T) { // Ensure middleware slices are not re-used e := New() g := e.Group("/group") h := func(Context) error { return nil } m1 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return next(c) } } m2 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return next(c) } } m3 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return next(c) } } m4 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return c.NoContent(404) } } m5 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return c.NoContent(405) } } g.Use(m1, m2, m3) g.GET("/404", h, m4) g.GET("/405", h, m5) c, _ := request(http.MethodGet, "/group/404", e) assert.Equal(t, 404, c) c, _ = request(http.MethodGet, "/group/405", e) assert.Equal(t, 405, c) } func TestGroupRouteMiddlewareWithMatchAny(t *testing.T) { // Ensure middleware and match any routes do not conflict e := New() g := e.Group("/group") m1 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return next(c) } } m2 := func(next HandlerFunc) HandlerFunc { return func(c Context) error { return c.String(http.StatusOK, c.Path()) } } h := func(c Context) error { return c.String(http.StatusOK, c.Path()) } g.Use(m1) g.GET("/help", h, m2) g.GET("/*", h, m2) g.GET("", h, m2) e.GET("unrelated", h, m2) e.GET("*", h, m2) _, m := request(http.MethodGet, "/group/help", e) assert.Equal(t, "/group/help", m) _, m = request(http.MethodGet, "/group/help/other", e) assert.Equal(t, "/group/*", m) _, m = request(http.MethodGet, "/group/404", e) assert.Equal(t, "/group/*", m) _, m = request(http.MethodGet, "/group", e) assert.Equal(t, "/group", m) _, m = request(http.MethodGet, "/other", e) assert.Equal(t, "/*", m) _, m = request(http.MethodGet, "/", e) assert.Equal(t, "/*", m) } func TestGroup_RouteNotFound(t *testing.T) { var testCases = []struct { name string whenURL string expectRoute interface{} expectCode int }{ { name: "404, route to static not found handler /group/a/c/xx", whenURL: "/group/a/c/xx", expectRoute: "GET /group/a/c/xx", expectCode: http.StatusNotFound, }, { name: "404, route to path param not found handler /group/a/:file", whenURL: "/group/a/echo.exe", expectRoute: "GET /group/a/:file", expectCode: http.StatusNotFound, }, { name: "404, route to any not found handler /group/*", whenURL: "/group/b/echo.exe", expectRoute: "GET /group/*", expectCode: http.StatusNotFound, }, { name: "200, route /group/a/c/df to /group/a/c/df", whenURL: "/group/a/c/df", expectRoute: "GET /group/a/c/df", expectCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := New() g := e.Group("/group") okHandler := func(c Context) error { return c.String(http.StatusOK, c.Request().Method+" "+c.Path()) } notFoundHandler := func(c Context) error { return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path()) } g.GET("/", okHandler) g.GET("/a/c/df", okHandler) g.GET("/a/b*", okHandler) g.PUT("/*", okHandler) g.RouteNotFound("/a/c/xx", notFoundHandler) // static g.RouteNotFound("/a/:file", notFoundHandler) // param g.RouteNotFound("/*", notFoundHandler) // any req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectCode, rec.Code) assert.Equal(t, tc.expectRoute, rec.Body.String()) }) } } func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) { var testCases = []struct { name string givenCustom404 bool whenURL string expectBody interface{} expectCode int }{ { name: "ok, custom 404 handler is called with middleware", givenCustom404: true, whenURL: "/group/test3", expectBody: "GET /group/*", expectCode: http.StatusNotFound, }, { name: "ok, default group 404 handler is called with middleware", givenCustom404: false, whenURL: "/group/test3", expectBody: "{\"message\":\"Not Found\"}\n", expectCode: http.StatusNotFound, }, { name: "ok, (no slash) default group 404 handler is called with middleware", givenCustom404: false, whenURL: "/group", expectBody: "{\"message\":\"Not Found\"}\n", expectCode: http.StatusNotFound, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { okHandler := func(c Context) error { return c.String(http.StatusOK, c.Request().Method+" "+c.Path()) } notFoundHandler := func(c Context) error { return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path()) } e := New() e.GET("/test1", okHandler) e.RouteNotFound("/*", notFoundHandler) g := e.Group("/group") g.GET("/test1", okHandler) middlewareCalled := false g.Use(func(next HandlerFunc) HandlerFunc { return func(c Context) error { middlewareCalled = true return next(c) } }) if tc.givenCustom404 { g.RouteNotFound("/*", notFoundHandler) } req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.True(t, middlewareCalled) assert.Equal(t, tc.expectCode, rec.Code) assert.Equal(t, tc.expectBody, rec.Body.String()) }) } } golang-github-labstack-echo-4.11.1/ip.go000066400000000000000000000230511445647333300177770ustar00rootroot00000000000000package echo import ( "net" "net/http" "strings" ) /** By: https://github.com/tmshn (See: https://github.com/labstack/echo/pull/1478 , https://github.com/labstack/echox/pull/134 ) Source: https://echo.labstack.com/guide/ip-address/ IP address plays fundamental role in HTTP; it's used for access control, auditing, geo-based access analysis and more. Echo provides handy method [`Context#RealIP()`](https://godoc.org/github.com/labstack/echo#Context) for that. However, it is not trivial to retrieve the _real_ IP address from requests especially when you put L7 proxies before the application. In such situation, _real_ IP needs to be relayed on HTTP layer from proxies to your app, but you must not trust HTTP headers unconditionally. Otherwise, you might give someone a chance of deceiving you. **A security risk!** To retrieve IP address reliably/securely, you must let your application be aware of the entire architecture of your infrastructure. In Echo, this can be done by configuring `Echo#IPExtractor` appropriately. This guides show you why and how. > Note: if you dont' set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice. Let's start from two questions to know the right direction: 1. Do you put any HTTP (L7) proxy in front of the application? - It includes both cloud solutions (such as AWS ALB or GCP HTTP LB) and OSS ones (such as Nginx, Envoy or Istio ingress gateway). 2. If yes, what HTTP header do your proxies use to pass client IP to the application? ## Case 1. With no proxy If you put no proxy (e.g.: directory facing to the internet), all you need to (and have to) see is IP address from network layer. Any HTTP header is untrustable because the clients have full control what headers to be set. In this case, use `echo.ExtractIPDirect()`. ```go e.IPExtractor = echo.ExtractIPDirect() ``` ## Case 2. With proxies using `X-Forwarded-For` header [`X-Forwared-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) is the popular header to relay clients' IP addresses. At each hop on the proxies, they append the request IP address at the end of the header. Following example diagram illustrates this behavior. ```text ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ "Origin" │───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ │ (IP: a) │ │ (IP: b) │ │ (IP: c) │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ Case 1. XFF: "" "a" "a, b" ~~~~~~ Case 2. XFF: "x" "x, a" "x, a, b" ~~~~~~~~~ ↑ What your app will see ``` In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructre". In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`. In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`. ```go e.IPExtractor = echo.ExtractIPFromXFFHeader() ``` By default, it trusts internal IP addresses (loopback, link-local unicast, private-use and unique local address from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and [RFC4193](https://tools.ietf.org/html/rfc4193)). To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s. E.g.: ```go e.IPExtractor = echo.ExtractIPFromXFFHeader( TrustLinkLocal(false), TrustIPRanges(lbIPRange), ) ``` - Ref: https://godoc.org/github.com/labstack/echo#TrustOption ## Case 3. With proxies using `X-Real-IP` header `X-Real-IP` is another HTTP header to relay clients' IP addresses, but it carries only one address unlike XFF. If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`. ```go e.IPExtractor = echo.ExtractIPFromRealIPHeader() ``` Again, it trusts internal IP addresses by default (loopback, link-local unicast, private-use and unique local address from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and [RFC4193](https://tools.ietf.org/html/rfc4193)). To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s. - Ref: https://godoc.org/github.com/labstack/echo#TrustOption > **Never forget** to configure the outermost proxy (i.e.; at the edge of your infrastructure) **not to pass through incoming headers**. > Otherwise there is a chance of fraud, as it is what clients can control. ## About default behavior In default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer. As you might already notice, after reading this article, this is not good. Sole reason this is default is just backward compatibility. ## Private IP ranges See: https://en.wikipedia.org/wiki/Private_network Private IPv4 address ranges (RFC 1918): * 10.0.0.0 – 10.255.255.255 (24-bit block) * 172.16.0.0 – 172.31.255.255 (20-bit block) * 192.168.0.0 – 192.168.255.255 (16-bit block) Private IPv6 address ranges: * fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA) */ type ipChecker struct { trustLoopback bool trustLinkLocal bool trustPrivateNet bool trustExtraRanges []*net.IPNet } // TrustOption is config for which IP address to trust type TrustOption func(*ipChecker) // TrustLoopback configures if you trust loopback address (default: true). func TrustLoopback(v bool) TrustOption { return func(c *ipChecker) { c.trustLoopback = v } } // TrustLinkLocal configures if you trust link-local address (default: true). func TrustLinkLocal(v bool) TrustOption { return func(c *ipChecker) { c.trustLinkLocal = v } } // TrustPrivateNet configures if you trust private network address (default: true). func TrustPrivateNet(v bool) TrustOption { return func(c *ipChecker) { c.trustPrivateNet = v } } // TrustIPRange add trustable IP ranges using CIDR notation. func TrustIPRange(ipRange *net.IPNet) TrustOption { return func(c *ipChecker) { c.trustExtraRanges = append(c.trustExtraRanges, ipRange) } } func newIPChecker(configs []TrustOption) *ipChecker { checker := &ipChecker{trustLoopback: true, trustLinkLocal: true, trustPrivateNet: true} for _, configure := range configs { configure(checker) } return checker } // Go1.16+ added `ip.IsPrivate()` but until that use this implementation func isPrivateIPRange(ip net.IP) bool { if ip4 := ip.To4(); ip4 != nil { return ip4[0] == 10 || ip4[0] == 172 && ip4[1]&0xf0 == 16 || ip4[0] == 192 && ip4[1] == 168 } return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc } func (c *ipChecker) trust(ip net.IP) bool { if c.trustLoopback && ip.IsLoopback() { return true } if c.trustLinkLocal && ip.IsLinkLocalUnicast() { return true } if c.trustPrivateNet && isPrivateIPRange(ip) { return true } for _, trustedRange := range c.trustExtraRanges { if trustedRange.Contains(ip) { return true } } return false } // IPExtractor is a function to extract IP addr from http.Request. // Set appropriate one to Echo#IPExtractor. // See https://echo.labstack.com/guide/ip-address for more details. type IPExtractor func(*http.Request) string // ExtractIPDirect extracts IP address using actual IP address. // Use this if your server faces to internet directory (i.e.: uses no proxy). func ExtractIPDirect() IPExtractor { return extractIP } func extractIP(req *http.Request) string { ra, _, _ := net.SplitHostPort(req.RemoteAddr) return ra } // ExtractIPFromRealIPHeader extracts IP address using x-real-ip header. // Use this if you put proxy which uses this header. func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor { checker := newIPChecker(options) return func(req *http.Request) string { realIP := req.Header.Get(HeaderXRealIP) if realIP != "" { realIP = strings.TrimPrefix(realIP, "[") realIP = strings.TrimSuffix(realIP, "]") if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) { return realIP } } return extractIP(req) } } // ExtractIPFromXFFHeader extracts IP address using x-forwarded-for header. // Use this if you put proxy which uses this header. // This returns nearest untrustable IP. If all IPs are trustable, returns furthest one (i.e.: XFF[0]). func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor { checker := newIPChecker(options) return func(req *http.Request) string { directIP := extractIP(req) xffs := req.Header[HeaderXForwardedFor] if len(xffs) == 0 { return directIP } ips := append(strings.Split(strings.Join(xffs, ","), ","), directIP) for i := len(ips) - 1; i >= 0; i-- { ips[i] = strings.TrimSpace(ips[i]) ips[i] = strings.TrimPrefix(ips[i], "[") ips[i] = strings.TrimSuffix(ips[i], "]") ip := net.ParseIP(ips[i]) if ip == nil { // Unable to parse IP; cannot trust entire records return directIP } if !checker.trust(ip) { return ip.String() } } // All of the IPs are trusted; return first element because it is furthest from server (best effort strategy). return strings.TrimSpace(ips[0]) } } golang-github-labstack-echo-4.11.1/ip_test.go000066400000000000000000000504341445647333300210430ustar00rootroot00000000000000package echo import ( "github.com/stretchr/testify/assert" "net" "net/http" "testing" ) func mustParseCIDR(s string) *net.IPNet { _, IPNet, err := net.ParseCIDR(s) if err != nil { panic(err) } return IPNet } func TestIPChecker_TrustOption(t *testing.T) { var testCases = []struct { name string givenOptions []TrustOption whenIP string expect bool }{ { name: "ip is within trust range, trusts additional private IPV6 network", givenOptions: []TrustOption{ TrustLoopback(false), TrustLinkLocal(false), TrustPrivateNet(false), // this is private IPv6 ip // CIDR Notation: 2001:0db8:0000:0000:0000:0000:0000:0000/48 // Address: 2001:0db8:0000:0000:0000:0000:0000:0103 // Range start: 2001:0db8:0000:0000:0000:0000:0000:0000 // Range end: 2001:0db8:0000:ffff:ffff:ffff:ffff:ffff TrustIPRange(mustParseCIDR("2001:db8::103/48")), }, whenIP: "2001:0db8:0000:0000:0000:0000:0000:0103", expect: true, }, { name: "ip is within trust range, trusts additional private IPV6 network", givenOptions: []TrustOption{ TrustIPRange(mustParseCIDR("2001:db8::103/48")), }, whenIP: "2001:0db8:0000:0000:0000:0000:0000:0103", expect: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checker := newIPChecker(tc.givenOptions) result := checker.trust(net.ParseIP(tc.whenIP)) assert.Equal(t, tc.expect, result) }) } } func TestTrustIPRange(t *testing.T) { var testCases = []struct { name string givenRange string whenIP string expect bool }{ { name: "ip is within trust range, IPV6 network range", // CIDR Notation: 2001:0db8:0000:0000:0000:0000:0000:0000/48 // Address: 2001:0db8:0000:0000:0000:0000:0000:0103 // Range start: 2001:0db8:0000:0000:0000:0000:0000:0000 // Range end: 2001:0db8:0000:ffff:ffff:ffff:ffff:ffff givenRange: "2001:db8::103/48", whenIP: "2001:0db8:0000:0000:0000:0000:0000:0103", expect: true, }, { name: "ip is outside (upper bounds) of trust range, IPV6 network range", givenRange: "2001:db8::103/48", whenIP: "2001:0db8:0001:0000:0000:0000:0000:0000", expect: false, }, { name: "ip is outside (lower bounds) of trust range, IPV6 network range", givenRange: "2001:db8::103/48", whenIP: "2001:0db7:ffff:ffff:ffff:ffff:ffff:ffff", expect: false, }, { name: "ip is within trust range, IPV4 network range", // CIDR Notation: 8.8.8.8/24 // Address: 8.8.8.8 // Range start: 8.8.8.0 // Range end: 8.8.8.255 givenRange: "8.8.8.0/24", whenIP: "8.8.8.8", expect: true, }, { name: "ip is within trust range, IPV4 network range", // CIDR Notation: 8.8.8.8/24 // Address: 8.8.8.8 // Range start: 8.8.8.0 // Range end: 8.8.8.255 givenRange: "8.8.8.0/24", whenIP: "8.8.8.8", expect: true, }, { name: "ip is outside (upper bounds) of trust range, IPV4 network range", givenRange: "8.8.8.0/24", whenIP: "8.8.9.0", expect: false, }, { name: "ip is outside (lower bounds) of trust range, IPV4 network range", givenRange: "8.8.8.0/24", whenIP: "8.8.7.255", expect: false, }, { name: "public ip, trust everything in IPV4 network range", givenRange: "0.0.0.0/0", whenIP: "8.8.8.8", expect: true, }, { name: "internal ip, trust everything in IPV4 network range", givenRange: "0.0.0.0/0", whenIP: "127.0.10.1", expect: true, }, { name: "public ip, trust everything in IPV6 network range", givenRange: "::/0", whenIP: "2a00:1450:4026:805::200e", expect: true, }, { name: "internal ip, trust everything in IPV6 network range", givenRange: "::/0", whenIP: "0:0:0:0:0:0:0:1", expect: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cidr := mustParseCIDR(tc.givenRange) checker := newIPChecker([]TrustOption{ TrustLoopback(false), // disable to avoid interference TrustLinkLocal(false), // disable to avoid interference TrustPrivateNet(false), // disable to avoid interference TrustIPRange(cidr), }) result := checker.trust(net.ParseIP(tc.whenIP)) assert.Equal(t, tc.expect, result) }) } } func TestTrustPrivateNet(t *testing.T) { var testCases = []struct { name string whenIP string expect bool }{ { name: "do not trust public IPv4 address", whenIP: "8.8.8.8", expect: false, }, { name: "do not trust public IPv6 address", whenIP: "2a00:1450:4026:805::200e", expect: false, }, { // Class A: 10.0.0.0 — 10.255.255.255 name: "do not trust IPv4 just outside of class A (lower bounds)", whenIP: "9.255.255.255", expect: false, }, { name: "do not trust IPv4 just outside of class A (upper bounds)", whenIP: "11.0.0.0", expect: false, }, { name: "trust IPv4 of class A (lower bounds)", whenIP: "10.0.0.0", expect: true, }, { name: "trust IPv4 of class A (upper bounds)", whenIP: "10.255.255.255", expect: true, }, { // Class B: 172.16.0.0 — 172.31.255.255 name: "do not trust IPv4 just outside of class B (lower bounds)", whenIP: "172.15.255.255", expect: false, }, { name: "do not trust IPv4 just outside of class B (upper bounds)", whenIP: "172.32.0.0", expect: false, }, { name: "trust IPv4 of class B (lower bounds)", whenIP: "172.16.0.0", expect: true, }, { name: "trust IPv4 of class B (upper bounds)", whenIP: "172.31.255.255", expect: true, }, { // Class C: 192.168.0.0 — 192.168.255.255 name: "do not trust IPv4 just outside of class C (lower bounds)", whenIP: "192.167.255.255", expect: false, }, { name: "do not trust IPv4 just outside of class C (upper bounds)", whenIP: "192.169.0.0", expect: false, }, { name: "trust IPv4 of class C (lower bounds)", whenIP: "192.168.0.0", expect: true, }, { name: "trust IPv4 of class C (upper bounds)", whenIP: "192.168.255.255", expect: true, }, { // fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA) // splits the address block in two equally sized halves, fc00::/8 and fd00::/8. // https://en.wikipedia.org/wiki/Unique_local_address name: "trust IPv6 private address", whenIP: "fdfc:3514:2cb3:4bd5::", expect: true, }, { name: "do not trust IPv6 just out of /fd (upper bounds)", whenIP: "/fe00:0000:0000:0000:0000", expect: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checker := newIPChecker([]TrustOption{ TrustLoopback(false), // disable to avoid interference TrustLinkLocal(false), // disable to avoid interference TrustPrivateNet(true), }) result := checker.trust(net.ParseIP(tc.whenIP)) assert.Equal(t, tc.expect, result) }) } } func TestTrustLinkLocal(t *testing.T) { var testCases = []struct { name string whenIP string expect bool }{ { name: "trust link local IPv4 address (lower bounds)", whenIP: "169.254.0.0", expect: true, }, { name: "trust link local IPv4 address (upper bounds)", whenIP: "169.254.255.255", expect: true, }, { name: "do not trust link local IPv4 address (outside of lower bounds)", whenIP: "169.253.255.255", expect: false, }, { name: "do not trust link local IPv4 address (outside of upper bounds)", whenIP: "169.255.0.0", expect: false, }, { name: "trust link local IPv6 address ", whenIP: "fe80::1", expect: true, }, { name: "do not trust link local IPv6 address ", whenIP: "fec0::1", expect: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checker := newIPChecker([]TrustOption{ TrustLoopback(false), // disable to avoid interference TrustPrivateNet(false), // disable to avoid interference TrustLinkLocal(true), }) result := checker.trust(net.ParseIP(tc.whenIP)) assert.Equal(t, tc.expect, result) }) } } func TestTrustLoopback(t *testing.T) { var testCases = []struct { name string whenIP string expect bool }{ { name: "trust IPv4 as localhost", whenIP: "127.0.0.1", expect: true, }, { name: "trust IPv6 as localhost", whenIP: "::1", expect: true, }, { name: "do not trust public ip as localhost", whenIP: "8.8.8.8", expect: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checker := newIPChecker([]TrustOption{ TrustLinkLocal(false), // disable to avoid interference TrustPrivateNet(false), // disable to avoid interference TrustLoopback(true), }) result := checker.trust(net.ParseIP(tc.whenIP)) assert.Equal(t, tc.expect, result) }) } } func TestExtractIPDirect(t *testing.T) { var testCases = []struct { name string whenRequest http.Request expectIP string }{ { name: "request has no headers, extracts IP from request remote addr", whenRequest: http.Request{ RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from external IP has X-Real-Ip header, extractor still extracts IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.10"}, }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from internal IP and has Real-IP header, extractor still extracts internal IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.10"}, }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.1", }, { name: "request is from external IP and has XFF + Real-IP header, extractor still extracts external IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.10"}, HeaderXForwardedFor: []string{"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101"}, }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from internal IP and has XFF + Real-IP header, extractor still extracts internal IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"127.0.0.1"}, HeaderXForwardedFor: []string{"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101"}, }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.1", }, { name: "request is from external IP and has XFF header, extractor still extracts external IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101"}, }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from internal IP and has XFF header, extractor still extracts internal IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101"}, }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.1", }, { name: "request is from internal IP and has INVALID XFF header, extractor still extracts internal IP from request remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"this.is.broken.lol, 169.254.0.101"}, }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.1", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { extractedIP := ExtractIPDirect()(&tc.whenRequest) assert.Equal(t, tc.expectIP, extractedIP) }) } } func TestExtractIPFromRealIPHeader(t *testing.T) { _, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24") _, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64") var testCases = []struct { name string givenTrustOptions []TrustOption whenRequest http.Request expectIP string }{ { name: "request has no headers, extracts IP from request remote addr", whenRequest: http.Request{ RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from external IP has INVALID external X-Real-Ip header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"xxx.yyy.zzz.ccc"}, // <-- this is invalid }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.199"}, // <-- this is untrusted }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"[2001:db8::113:199]"}, // <-- this is untrusted }, RemoteAddr: "[2001:db8::113:1]:8080", }, expectIP: "2001:db8::113:1", }, { name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header", givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy" TrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range "203.0.113.199/24" }, whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.199"}, }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.199", }, { name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header", givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy" TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64" }, whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"[2001:db8::113:199]"}, }, RemoteAddr: "[2001:db8::113:1]:8080", }, expectIP: "2001:db8::113:199", }, { name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header", givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy" TrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range "203.0.113.199/24" }, whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"203.0.113.199"}, HeaderXForwardedFor: []string{"203.0.113.198, 203.0.113.197"}, // <-- should not affect anything }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.199", }, { name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header", givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy" TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64" }, whenRequest: http.Request{ Header: http.Header{ HeaderXRealIP: []string{"[2001:db8::113:199]"}, HeaderXForwardedFor: []string{"[2001:db8::113:198], [2001:db8::113:197]"}, // <-- should not affect anything }, RemoteAddr: "[2001:db8::113:1]:8080", }, expectIP: "2001:db8::113:199", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { extractedIP := ExtractIPFromRealIPHeader(tc.givenTrustOptions...)(&tc.whenRequest) assert.Equal(t, tc.expectIP, extractedIP) }) } } func TestExtractIPFromXFFHeader(t *testing.T) { _, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24") _, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64") var testCases = []struct { name string givenTrustOptions []TrustOption whenRequest http.Request expectIP string }{ { name: "request has no headers, extracts IP from request remote addr", whenRequest: http.Request{ RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request has INVALID external XFF header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"xxx.yyy.zzz.ccc, 127.0.0.2"}, // <-- this is invalid }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.1", }, { name: "request trusts all IPs in XFF header, extract IP from furthest in XFF chain", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"127.0.0.3, 127.0.0.2, 127.0.0.1"}, }, RemoteAddr: "127.0.0.1:8080", }, expectIP: "127.0.0.3", }, { name: "request trusts all IPs in XFF header, extract IP from furthest in XFF chain", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"[fe80::3], [fe80::2], [fe80::1]"}, }, RemoteAddr: "[fe80::1]:8080", }, expectIP: "fe80::3", }, { name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"203.0.113.199"}, // <-- this is untrusted }, RemoteAddr: "203.0.113.1:8080", }, expectIP: "203.0.113.1", }, { name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr", whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"[2001:db8::1]"}, // <-- this is untrusted }, RemoteAddr: "[2001:db8::2]:8080", }, expectIP: "2001:db8::2", }, { name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header", givenTrustOptions: []TrustOption{ TrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range "203.0.113.199/24" }, // from request its seems that request has been proxied through 6 servers. // 1) 203.0.1.100 (this is external IP set by 203.0.100.100 which we do not trust - could be spoofed) // 2) 203.0.100.100 (this is outside of our network but set by 203.0.113.199 which we trust to set correct IPs) // 3) 203.0.113.199 (we trust, for example maybe our proxy from some other office) // 4) 192.168.1.100 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products) // 5) 127.0.0.1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing) whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"203.0.1.100, 203.0.100.100, 203.0.113.199, 192.168.1.100"}, }, RemoteAddr: "127.0.0.1:8080", // IP of proxy upstream of our APP }, expectIP: "203.0.100.100", // this is first trusted IP in XFF chain }, { name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header", givenTrustOptions: []TrustOption{ TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64" }, // from request its seems that request has been proxied through 6 servers. // 1) 2001:db8:1::1:100 (this is external IP set by 2001:db8:2::100:100 which we do not trust - could be spoofed) // 2) 2001:db8:2::100:100 (this is outside of our network but set by 2001:db8::113:199 which we trust to set correct IPs) // 3) 2001:db8::113:199 (we trust, for example maybe our proxy from some other office) // 4) fd12:3456:789a:1::1 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products) // 5) fe80::1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing) whenRequest: http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{"[2001:db8:1::1:100], [2001:db8:2::100:100], [2001:db8::113:199], [fd12:3456:789a:1::1]"}, }, RemoteAddr: "[fe80::1]:8080", // IP of proxy upstream of our APP }, expectIP: "2001:db8:2::100:100", // this is first trusted IP in XFF chain }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { extractedIP := ExtractIPFromXFFHeader(tc.givenTrustOptions...)(&tc.whenRequest) assert.Equal(t, tc.expectIP, extractedIP) }) } } golang-github-labstack-echo-4.11.1/json.go000066400000000000000000000022271445647333300203420ustar00rootroot00000000000000package echo import ( "encoding/json" "fmt" "net/http" ) // DefaultJSONSerializer implements JSON encoding using encoding/json. type DefaultJSONSerializer struct{} // Serialize converts an interface into a json and writes it to the response. // You can optionally use the indent parameter to produce pretty JSONs. func (d DefaultJSONSerializer) Serialize(c Context, i interface{}, indent string) error { enc := json.NewEncoder(c.Response()) if indent != "" { enc.SetIndent("", indent) } return enc.Encode(i) } // Deserialize reads a JSON from a request body and converts it into an interface. func (d DefaultJSONSerializer) Deserialize(c Context, i interface{}) error { err := json.NewDecoder(c.Request().Body).Decode(i) if ute, ok := err.(*json.UnmarshalTypeError); ok { return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err) } else if se, ok := err.(*json.SyntaxError); ok { return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err) } return err } golang-github-labstack-echo-4.11.1/json_test.go000066400000000000000000000054131445647333300214010ustar00rootroot00000000000000package echo import ( testify "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "strings" "testing" ) // Note this test is deliberately simple as there's not a lot to test. // Just need to ensure it writes JSONs. The heavy work is done by the context methods. func TestDefaultJSONCodec_Encode(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodPost, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) assert := testify.New(t) // Echo assert.Equal(e, c.Echo()) // Request assert.NotNil(c.Request()) // Response assert.NotNil(c.Response()) //-------- // Default JSON encoder //-------- enc := new(DefaultJSONSerializer) err := enc.Serialize(c, user{1, "Jon Snow"}, "") if assert.NoError(err) { assert.Equal(userJSON+"\n", rec.Body.String()) } req = httptest.NewRequest(http.MethodPost, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = enc.Serialize(c, user{1, "Jon Snow"}, " ") if assert.NoError(err) { assert.Equal(userJSONPretty+"\n", rec.Body.String()) } } // Note this test is deliberately simple as there's not a lot to test. // Just need to ensure it writes JSONs. The heavy work is done by the context methods. func TestDefaultJSONCodec_Decode(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := e.NewContext(req, rec).(*context) assert := testify.New(t) // Echo assert.Equal(e, c.Echo()) // Request assert.NotNil(c.Request()) // Response assert.NotNil(c.Response()) //-------- // Default JSON encoder //-------- enc := new(DefaultJSONSerializer) var u = user{} err := enc.Deserialize(c, &u) if assert.NoError(err) { assert.Equal(u, user{ID: 1, Name: "Jon Snow"}) } var userUnmarshalSyntaxError = user{} req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(invalidContent)) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = enc.Deserialize(c, &userUnmarshalSyntaxError) assert.IsType(&HTTPError{}, err) assert.EqualError(err, "code=400, message=Syntax error: offset=1, error=invalid character 'i' looking for beginning of value, internal=invalid character 'i' looking for beginning of value") var userUnmarshalTypeError = struct { ID string `json:"id"` Name string `json:"name"` }{} req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) rec = httptest.NewRecorder() c = e.NewContext(req, rec).(*context) err = enc.Deserialize(c, &userUnmarshalTypeError) assert.IsType(&HTTPError{}, err) assert.EqualError(err, "code=400, message=Unmarshal type error: expected=string, got=number, field=id, offset=7, internal=json: cannot unmarshal number into Go struct field .id of type string") } golang-github-labstack-echo-4.11.1/log.go000066400000000000000000000016371445647333300201560ustar00rootroot00000000000000package echo import ( "io" "github.com/labstack/gommon/log" ) type ( // Logger defines the logging interface. Logger interface { Output() io.Writer SetOutput(w io.Writer) Prefix() string SetPrefix(p string) Level() log.Lvl SetLevel(v log.Lvl) SetHeader(h string) Print(i ...interface{}) Printf(format string, args ...interface{}) Printj(j log.JSON) Debug(i ...interface{}) Debugf(format string, args ...interface{}) Debugj(j log.JSON) Info(i ...interface{}) Infof(format string, args ...interface{}) Infoj(j log.JSON) Warn(i ...interface{}) Warnf(format string, args ...interface{}) Warnj(j log.JSON) Error(i ...interface{}) Errorf(format string, args ...interface{}) Errorj(j log.JSON) Fatal(i ...interface{}) Fatalj(j log.JSON) Fatalf(format string, args ...interface{}) Panic(i ...interface{}) Panicj(j log.JSON) Panicf(format string, args ...interface{}) } ) golang-github-labstack-echo-4.11.1/middleware/000077500000000000000000000000001445647333300211545ustar00rootroot00000000000000golang-github-labstack-echo-4.11.1/middleware/basic_auth.go000066400000000000000000000053321445647333300236100ustar00rootroot00000000000000package middleware import ( "encoding/base64" "net/http" "strconv" "strings" "github.com/labstack/echo/v4" ) type ( // BasicAuthConfig defines the config for BasicAuth middleware. BasicAuthConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Validator is a function to validate BasicAuth credentials. // Required. Validator BasicAuthValidator // Realm is a string to define realm attribute of BasicAuth. // Default value "Restricted". Realm string } // BasicAuthValidator defines a function to validate BasicAuth credentials. BasicAuthValidator func(string, string, echo.Context) (bool, error) ) const ( basic = "basic" defaultRealm = "Restricted" ) var ( // DefaultBasicAuthConfig is the default BasicAuth middleware config. DefaultBasicAuthConfig = BasicAuthConfig{ Skipper: DefaultSkipper, Realm: defaultRealm, } ) // BasicAuth returns an BasicAuth middleware. // // For valid credentials it calls the next handler. // For missing or invalid credentials, it sends "401 - Unauthorized" response. func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc { c := DefaultBasicAuthConfig c.Validator = fn return BasicAuthWithConfig(c) } // BasicAuthWithConfig returns an BasicAuth middleware with config. // See `BasicAuth()`. func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { // Defaults if config.Validator == nil { panic("echo: basic-auth middleware requires a validator function") } if config.Skipper == nil { config.Skipper = DefaultBasicAuthConfig.Skipper } if config.Realm == "" { config.Realm = defaultRealm } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } auth := c.Request().Header.Get(echo.HeaderAuthorization) l := len(basic) if len(auth) > l+1 && strings.EqualFold(auth[:l], basic) { // Invalid base64 shouldn't be treated as error // instead should be treated as invalid client input b, err := base64.StdEncoding.DecodeString(auth[l+1:]) if err != nil { return echo.NewHTTPError(http.StatusBadRequest).SetInternal(err) } cred := string(b) for i := 0; i < len(cred); i++ { if cred[i] == ':' { // Verify credentials valid, err := config.Validator(cred[:i], cred[i+1:], c) if err != nil { return err } else if valid { return next(c) } break } } } realm := defaultRealm if config.Realm != defaultRealm { realm = strconv.Quote(config.Realm) } // Need to return `401` for browsers to pop-up login box. c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm="+realm) return echo.ErrUnauthorized } } } golang-github-labstack-echo-4.11.1/middleware/basic_auth_test.go000066400000000000000000000042371445647333300246520ustar00rootroot00000000000000package middleware import ( "encoding/base64" "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestBasicAuth(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) f := func(u, p string, c echo.Context) (bool, error) { if u == "joe" && p == "secret" { return true, nil } return false, nil } h := BasicAuth(f)(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) // Valid credentials auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret")) req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(t, h(c)) h = BasicAuthWithConfig(BasicAuthConfig{ Skipper: nil, Validator: f, Realm: "someRealm", })(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) // Valid credentials auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret")) req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(t, h(c)) // Case-insensitive header scheme auth = strings.ToUpper(basic) + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret")) req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(t, h(c)) // Invalid credentials auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:invalid-password")) req.Header.Set(echo.HeaderAuthorization, auth) he := h(c).(*echo.HTTPError) assert.Equal(t, http.StatusUnauthorized, he.Code) assert.Equal(t, basic+` realm="someRealm"`, res.Header().Get(echo.HeaderWWWAuthenticate)) // Invalid base64 string auth = basic + " invalidString" req.Header.Set(echo.HeaderAuthorization, auth) he = h(c).(*echo.HTTPError) assert.Equal(t, http.StatusBadRequest, he.Code) // Missing Authorization header req.Header.Del(echo.HeaderAuthorization) he = h(c).(*echo.HTTPError) assert.Equal(t, http.StatusUnauthorized, he.Code) // Invalid Authorization header auth = base64.StdEncoding.EncodeToString([]byte("invalid")) req.Header.Set(echo.HeaderAuthorization, auth) he = h(c).(*echo.HTTPError) assert.Equal(t, http.StatusUnauthorized, he.Code) } golang-github-labstack-echo-4.11.1/middleware/body_dump.go000066400000000000000000000046401445647333300234710ustar00rootroot00000000000000package middleware import ( "bufio" "bytes" "io" "net" "net/http" "github.com/labstack/echo/v4" ) type ( // BodyDumpConfig defines the config for BodyDump middleware. BodyDumpConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Handler receives request and response payload. // Required. Handler BodyDumpHandler } // BodyDumpHandler receives the request and response payload. BodyDumpHandler func(echo.Context, []byte, []byte) bodyDumpResponseWriter struct { io.Writer http.ResponseWriter } ) var ( // DefaultBodyDumpConfig is the default BodyDump middleware config. DefaultBodyDumpConfig = BodyDumpConfig{ Skipper: DefaultSkipper, } ) // BodyDump returns a BodyDump middleware. // // BodyDump middleware captures the request and response payload and calls the // registered handler. func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc { c := DefaultBodyDumpConfig c.Handler = handler return BodyDumpWithConfig(c) } // BodyDumpWithConfig returns a BodyDump middleware with config. // See: `BodyDump()`. func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc { // Defaults if config.Handler == nil { panic("echo: body-dump middleware requires a handler function") } if config.Skipper == nil { config.Skipper = DefaultBodyDumpConfig.Skipper } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { if config.Skipper(c) { return next(c) } // Request reqBody := []byte{} if c.Request().Body != nil { // Read reqBody, _ = io.ReadAll(c.Request().Body) } c.Request().Body = io.NopCloser(bytes.NewBuffer(reqBody)) // Reset // Response resBody := new(bytes.Buffer) mw := io.MultiWriter(c.Response().Writer, resBody) writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer} c.Response().Writer = writer if err = next(c); err != nil { c.Error(err) } // Callback config.Handler(c, reqBody, resBody.Bytes()) return } } } func (w *bodyDumpResponseWriter) WriteHeader(code int) { w.ResponseWriter.WriteHeader(code) } func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } func (w *bodyDumpResponseWriter) Flush() { w.ResponseWriter.(http.Flusher).Flush() } func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.ResponseWriter.(http.Hijacker).Hijack() } golang-github-labstack-echo-4.11.1/middleware/body_dump_test.go000066400000000000000000000035701445647333300245310ustar00rootroot00000000000000package middleware import ( "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestBodyDump(t *testing.T) { e := echo.New() hw := "Hello, World!" req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(hw)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { return err } return c.String(http.StatusOK, string(body)) } requestBody := "" responseBody := "" mw := BodyDump(func(c echo.Context, reqBody, resBody []byte) { requestBody = string(reqBody) responseBody = string(resBody) }) if assert.NoError(t, mw(h)(c)) { assert.Equal(t, requestBody, hw) assert.Equal(t, responseBody, hw) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, hw, rec.Body.String()) } // Must set default skipper BodyDumpWithConfig(BodyDumpConfig{ Skipper: nil, Handler: func(c echo.Context, reqBody, resBody []byte) { requestBody = string(reqBody) responseBody = string(resBody) }, }) } func TestBodyDumpFails(t *testing.T) { e := echo.New() hw := "Hello, World!" req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(hw)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := func(c echo.Context) error { return errors.New("some error") } mw := BodyDump(func(c echo.Context, reqBody, resBody []byte) {}) if !assert.Error(t, mw(h)(c)) { t.FailNow() } assert.Panics(t, func() { mw = BodyDumpWithConfig(BodyDumpConfig{ Skipper: nil, Handler: nil, }) }) assert.NotPanics(t, func() { mw = BodyDumpWithConfig(BodyDumpConfig{ Skipper: func(c echo.Context) bool { return true }, Handler: func(c echo.Context, reqBody, resBody []byte) { }, }) if !assert.Error(t, mw(h)(c)) { t.FailNow() } }) } golang-github-labstack-echo-4.11.1/middleware/body_limit.go000066400000000000000000000053101445647333300236350ustar00rootroot00000000000000package middleware import ( "fmt" "io" "sync" "github.com/labstack/echo/v4" "github.com/labstack/gommon/bytes" ) type ( // BodyLimitConfig defines the config for BodyLimit middleware. BodyLimitConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Maximum allowed size for a request body, it can be specified // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. Limit string `yaml:"limit"` limit int64 } limitedReader struct { BodyLimitConfig reader io.ReadCloser read int64 context echo.Context } ) var ( // DefaultBodyLimitConfig is the default BodyLimit middleware config. DefaultBodyLimitConfig = BodyLimitConfig{ Skipper: DefaultSkipper, } ) // BodyLimit returns a BodyLimit middleware. // // BodyLimit middleware sets the maximum allowed size for a request body, if the // size exceeds the configured limit, it sends "413 - Request Entity Too Large" // response. The BodyLimit is determined based on both `Content-Length` request // header and actual content read, which makes it super secure. // Limit can be specified as `4x` or `4xB`, where x is one of the multiple from K, M, // G, T or P. func BodyLimit(limit string) echo.MiddlewareFunc { c := DefaultBodyLimitConfig c.Limit = limit return BodyLimitWithConfig(c) } // BodyLimitWithConfig returns a BodyLimit middleware with config. // See: `BodyLimit()`. func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultBodyLimitConfig.Skipper } limit, err := bytes.Parse(config.Limit) if err != nil { panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit)) } config.limit = limit pool := limitedReaderPool(config) return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() // Based on content length if req.ContentLength > config.limit { return echo.ErrStatusRequestEntityTooLarge } // Based on content read r := pool.Get().(*limitedReader) r.Reset(req.Body, c) defer pool.Put(r) req.Body = r return next(c) } } } func (r *limitedReader) Read(b []byte) (n int, err error) { n, err = r.reader.Read(b) r.read += int64(n) if r.read > r.limit { return n, echo.ErrStatusRequestEntityTooLarge } return } func (r *limitedReader) Close() error { return r.reader.Close() } func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) { r.reader = reader r.context = context r.read = 0 } func limitedReaderPool(c BodyLimitConfig) sync.Pool { return sync.Pool{ New: func() interface{} { return &limitedReader{BodyLimitConfig: c} }, } } golang-github-labstack-echo-4.11.1/middleware/body_limit_test.go000066400000000000000000000107401445647333300246770ustar00rootroot00000000000000package middleware import ( "bytes" "io" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestBodyLimit(t *testing.T) { e := echo.New() hw := []byte("Hello, World!") req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { return err } return c.String(http.StatusOK, string(body)) } // Based on content length (within limit) if assert.NoError(t, BodyLimit("2M")(h)(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, hw, rec.Body.Bytes()) } // Based on content length (overlimit) he := BodyLimit("2B")(h)(c).(*echo.HTTPError) assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code) // Based on content read (within limit) req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) req.ContentLength = -1 rec = httptest.NewRecorder() c = e.NewContext(req, rec) if assert.NoError(t, BodyLimit("2M")(h)(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "Hello, World!", rec.Body.String()) } // Based on content read (overlimit) req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) req.ContentLength = -1 rec = httptest.NewRecorder() c = e.NewContext(req, rec) he = BodyLimit("2B")(h)(c).(*echo.HTTPError) assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code) } func TestBodyLimitReader(t *testing.T) { hw := []byte("Hello, World!") e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) rec := httptest.NewRecorder() config := BodyLimitConfig{ Skipper: DefaultSkipper, Limit: "2B", limit: 2, } reader := &limitedReader{ BodyLimitConfig: config, reader: io.NopCloser(bytes.NewReader(hw)), context: e.NewContext(req, rec), } // read all should return ErrStatusRequestEntityTooLarge _, err := io.ReadAll(reader) he := err.(*echo.HTTPError) assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code) // reset reader and read two bytes must succeed bt := make([]byte, 2) reader.Reset(io.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec)) n, err := reader.Read(bt) assert.Equal(t, 2, n) assert.Equal(t, nil, err) } func TestBodyLimitWithConfig_Skipper(t *testing.T) { e := echo.New() h := func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { return err } return c.String(http.StatusOK, string(body)) } mw := BodyLimitWithConfig(BodyLimitConfig{ Skipper: func(c echo.Context) bool { return true }, Limit: "2B", // if not skipped this limit would make request to fail limit check }) hw := []byte("Hello, World!") req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := mw(h)(c) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, hw, rec.Body.Bytes()) } func TestBodyLimitWithConfig(t *testing.T) { var testCases = []struct { name string givenLimit string whenBody []byte expectBody []byte expectError string }{ { name: "ok, body is less than limit", givenLimit: "10B", whenBody: []byte("123456789"), expectBody: []byte("123456789"), expectError: "", }, { name: "nok, body is more than limit", givenLimit: "9B", whenBody: []byte("1234567890"), expectBody: []byte(nil), expectError: "code=413, message=Request Entity Too Large", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() h := func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { return err } return c.String(http.StatusOK, string(body)) } mw := BodyLimitWithConfig(BodyLimitConfig{ Limit: tc.givenLimit, }) req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.whenBody)) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := mw(h)(c) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } // not testing status as middlewares return error instead of committing it and OK cases are anyway 200 assert.Equal(t, tc.expectBody, rec.Body.Bytes()) }) } } func TestBodyLimit_panicOnInvalidLimit(t *testing.T) { assert.PanicsWithError( t, "echo: invalid body-limit=", func() { BodyLimit("") }, ) } golang-github-labstack-echo-4.11.1/middleware/compress.go000066400000000000000000000134051445647333300233410ustar00rootroot00000000000000package middleware import ( "bufio" "bytes" "compress/gzip" "io" "net" "net/http" "strings" "sync" "github.com/labstack/echo/v4" ) type ( // GzipConfig defines the config for Gzip middleware. GzipConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Gzip compression level. // Optional. Default value -1. Level int `yaml:"level"` // Length threshold before gzip compression is applied. // Optional. Default value 0. // // Most of the time you will not need to change the default. Compressing // a short response might increase the transmitted data because of the // gzip format overhead. Compressing the response will also consume CPU // and time on the server and the client (for decompressing). Depending on // your use case such a threshold might be useful. // // See also: // https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits MinLength int } gzipResponseWriter struct { io.Writer http.ResponseWriter wroteHeader bool wroteBody bool minLength int minLengthExceeded bool buffer *bytes.Buffer code int } ) const ( gzipScheme = "gzip" ) var ( // DefaultGzipConfig is the default Gzip middleware config. DefaultGzipConfig = GzipConfig{ Skipper: DefaultSkipper, Level: -1, MinLength: 0, } ) // Gzip returns a middleware which compresses HTTP response using gzip compression // scheme. func Gzip() echo.MiddlewareFunc { return GzipWithConfig(DefaultGzipConfig) } // GzipWithConfig return Gzip middleware with config. // See: `Gzip()`. func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultGzipConfig.Skipper } if config.Level == 0 { config.Level = DefaultGzipConfig.Level } if config.MinLength < 0 { config.MinLength = DefaultGzipConfig.MinLength } pool := gzipCompressPool(config) bpool := bufferPool() return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } res := c.Response() res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) { i := pool.Get() w, ok := i.(*gzip.Writer) if !ok { return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error()) } rw := res.Writer w.Reset(rw) buf := bpool.Get().(*bytes.Buffer) buf.Reset() grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf} defer func() { // There are different reasons for cases when we have not yet written response to the client and now need to do so. // a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now. // b) body is shorter than our minimum length threshold and being buffered currently and needs to be written if !grw.wroteBody { if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { res.Header().Del(echo.HeaderContentEncoding) } if grw.wroteHeader { rw.WriteHeader(grw.code) } // We have to reset response to it's pristine state when // nothing is written to body or error is returned. // See issue #424, #407. res.Writer = rw w.Reset(io.Discard) } else if !grw.minLengthExceeded { // Write uncompressed response res.Writer = rw if grw.wroteHeader { grw.ResponseWriter.WriteHeader(grw.code) } grw.buffer.WriteTo(rw) w.Reset(io.Discard) } w.Close() bpool.Put(buf) pool.Put(w) }() res.Writer = grw } return next(c) } } } func (w *gzipResponseWriter) WriteHeader(code int) { w.Header().Del(echo.HeaderContentLength) // Issue #444 w.wroteHeader = true // Delay writing of the header until we know if we'll actually compress the response w.code = code } func (w *gzipResponseWriter) Write(b []byte) (int, error) { if w.Header().Get(echo.HeaderContentType) == "" { w.Header().Set(echo.HeaderContentType, http.DetectContentType(b)) } w.wroteBody = true if !w.minLengthExceeded { n, err := w.buffer.Write(b) if w.buffer.Len() >= w.minLength { w.minLengthExceeded = true // The minimum length is exceeded, add Content-Encoding header and write the header w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806 if w.wroteHeader { w.ResponseWriter.WriteHeader(w.code) } return w.Writer.Write(w.buffer.Bytes()) } return n, err } return w.Writer.Write(b) } func (w *gzipResponseWriter) Flush() { if !w.minLengthExceeded { // Enforce compression because we will not know how much more data will come w.minLengthExceeded = true w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806 if w.wroteHeader { w.ResponseWriter.WriteHeader(w.code) } w.Writer.Write(w.buffer.Bytes()) } w.Writer.(*gzip.Writer).Flush() if flusher, ok := w.ResponseWriter.(http.Flusher); ok { flusher.Flush() } } func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.ResponseWriter.(http.Hijacker).Hijack() } func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error { if p, ok := w.ResponseWriter.(http.Pusher); ok { return p.Push(target, opts) } return http.ErrNotSupported } func gzipCompressPool(config GzipConfig) sync.Pool { return sync.Pool{ New: func() interface{} { w, err := gzip.NewWriterLevel(io.Discard, config.Level) if err != nil { return err } return w }, } } func bufferPool() sync.Pool { return sync.Pool{ New: func() interface{} { b := &bytes.Buffer{} return b }, } } golang-github-labstack-echo-4.11.1/middleware/compress_test.go000066400000000000000000000220611445647333300243760ustar00rootroot00000000000000package middleware import ( "bytes" "compress/gzip" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestGzip(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // Skip if no Accept-Encoding header h := Gzip()(func(c echo.Context) error { c.Response().Write([]byte("test")) // For Content-Type sniffing return nil }) h(c) assert.Equal(t, "test", rec.Body.String()) // Gzip req = httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h(c) assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) assert.Contains(t, rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain) r, err := gzip.NewReader(rec.Body) if assert.NoError(t, err) { buf := new(bytes.Buffer) defer r.Close() buf.ReadFrom(r) assert.Equal(t, "test", buf.String()) } chunkBuf := make([]byte, 5) // Gzip chunked req = httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec = httptest.NewRecorder() c = e.NewContext(req, rec) Gzip()(func(c echo.Context) error { c.Response().Header().Set("Content-Type", "text/event-stream") c.Response().Header().Set("Transfer-Encoding", "chunked") // Write and flush the first part of the data c.Response().Write([]byte("test\n")) c.Response().Flush() // Read the first part of the data assert.True(t, rec.Flushed) assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) r.Reset(rec.Body) _, err = io.ReadFull(r, chunkBuf) assert.NoError(t, err) assert.Equal(t, "test\n", string(chunkBuf)) // Write and flush the second part of the data c.Response().Write([]byte("test\n")) c.Response().Flush() _, err = io.ReadFull(r, chunkBuf) assert.NoError(t, err) assert.Equal(t, "test\n", string(chunkBuf)) // Write the final part of the data and return c.Response().Write([]byte("test")) return nil })(c) buf := new(bytes.Buffer) defer r.Close() buf.ReadFrom(r) assert.Equal(t, "test", buf.String()) } func TestGzipWithMinLength(t *testing.T) { e := echo.New() // Minimal response length e.Use(GzipWithConfig(GzipConfig{MinLength: 10})) e.GET("/", func(c echo.Context) error { c.Response().Write([]byte("foobarfoobar")) return nil }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) r, err := gzip.NewReader(rec.Body) if assert.NoError(t, err) { buf := new(bytes.Buffer) defer r.Close() buf.ReadFrom(r) assert.Equal(t, "foobarfoobar", buf.String()) } } func TestGzipWithMinLengthTooShort(t *testing.T) { e := echo.New() // Minimal response length e.Use(GzipWithConfig(GzipConfig{MinLength: 10})) e.GET("/", func(c echo.Context) error { c.Response().Write([]byte("test")) return nil }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding)) assert.Contains(t, rec.Body.String(), "test") } func TestGzipWithResponseWithoutBody(t *testing.T) { e := echo.New() e.Use(Gzip()) e.GET("/", func(c echo.Context) error { return c.Redirect(http.StatusMovedPermanently, "http://localhost") }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusMovedPermanently, rec.Code) assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding)) } func TestGzipWithMinLengthChunked(t *testing.T) { e := echo.New() // Gzip chunked chunkBuf := make([]byte, 5) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() var r *gzip.Reader = nil c := e.NewContext(req, rec) GzipWithConfig(GzipConfig{MinLength: 10})(func(c echo.Context) error { c.Response().Header().Set("Content-Type", "text/event-stream") c.Response().Header().Set("Transfer-Encoding", "chunked") // Write and flush the first part of the data c.Response().Write([]byte("test\n")) c.Response().Flush() // Read the first part of the data assert.True(t, rec.Flushed) assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) var err error r, err = gzip.NewReader(rec.Body) assert.NoError(t, err) _, err = io.ReadFull(r, chunkBuf) assert.NoError(t, err) assert.Equal(t, "test\n", string(chunkBuf)) // Write and flush the second part of the data c.Response().Write([]byte("test\n")) c.Response().Flush() _, err = io.ReadFull(r, chunkBuf) assert.NoError(t, err) assert.Equal(t, "test\n", string(chunkBuf)) // Write the final part of the data and return c.Response().Write([]byte("test")) return nil })(c) assert.NotNil(t, r) buf := new(bytes.Buffer) buf.ReadFrom(r) assert.Equal(t, "test", buf.String()) r.Close() } func TestGzipWithMinLengthNoContent(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := GzipWithConfig(GzipConfig{MinLength: 10})(func(c echo.Context) error { return c.NoContent(http.StatusNoContent) }) if assert.NoError(t, h(c)) { assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) assert.Empty(t, rec.Header().Get(echo.HeaderContentType)) assert.Equal(t, 0, len(rec.Body.Bytes())) } } func TestGzipNoContent(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Gzip()(func(c echo.Context) error { return c.NoContent(http.StatusNoContent) }) if assert.NoError(t, h(c)) { assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) assert.Empty(t, rec.Header().Get(echo.HeaderContentType)) assert.Equal(t, 0, len(rec.Body.Bytes())) } } func TestGzipEmpty(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Gzip()(func(c echo.Context) error { return c.String(http.StatusOK, "") }) if assert.NoError(t, h(c)) { assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) assert.Equal(t, "text/plain; charset=UTF-8", rec.Header().Get(echo.HeaderContentType)) r, err := gzip.NewReader(rec.Body) if assert.NoError(t, err) { var buf bytes.Buffer buf.ReadFrom(r) assert.Equal(t, "", buf.String()) } } } func TestGzipErrorReturned(t *testing.T) { e := echo.New() e.Use(Gzip()) e.GET("/", func(c echo.Context) error { return echo.ErrNotFound }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) } func TestGzipErrorReturnedInvalidConfig(t *testing.T) { e := echo.New() // Invalid level e.Use(GzipWithConfig(GzipConfig{Level: 12})) e.GET("/", func(c echo.Context) error { c.Response().Write([]byte("test")) return nil }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "gzip") } // Issue #806 func TestGzipWithStatic(t *testing.T) { e := echo.New() e.Use(Gzip()) e.Static("/test", "../_fixture/images") req := httptest.NewRequest(http.MethodGet, "/test/walle.png", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) // Data is written out in chunks when Content-Length == "", so only // validate the content length if it's not set. if cl := rec.Header().Get("Content-Length"); cl != "" { assert.Equal(t, cl, rec.Body.Len()) } r, err := gzip.NewReader(rec.Body) if assert.NoError(t, err) { defer r.Close() want, err := os.ReadFile("../_fixture/images/walle.png") if assert.NoError(t, err) { buf := new(bytes.Buffer) buf.ReadFrom(r) assert.Equal(t, want, buf.Bytes()) } } } func BenchmarkGzip(b *testing.B) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme) h := Gzip()(func(c echo.Context) error { c.Response().Write([]byte("test")) // For Content-Type sniffing return nil }) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { // Gzip rec := httptest.NewRecorder() c := e.NewContext(req, rec) h(c) } } golang-github-labstack-echo-4.11.1/middleware/context_timeout.go000066400000000000000000000036651445647333300247470ustar00rootroot00000000000000package middleware import ( "context" "errors" "time" "github.com/labstack/echo/v4" ) // ContextTimeoutConfig defines the config for ContextTimeout middleware. type ContextTimeoutConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // ErrorHandler is a function when error aries in middeware execution. ErrorHandler func(err error, c echo.Context) error // Timeout configures a timeout for the middleware, defaults to 0 for no timeout Timeout time.Duration } // ContextTimeout returns a middleware which returns error (503 Service Unavailable error) to client // when underlying method returns context.DeadlineExceeded error. func ContextTimeout(timeout time.Duration) echo.MiddlewareFunc { return ContextTimeoutWithConfig(ContextTimeoutConfig{Timeout: timeout}) } // ContextTimeoutWithConfig returns a Timeout middleware with config. func ContextTimeoutWithConfig(config ContextTimeoutConfig) echo.MiddlewareFunc { mw, err := config.ToMiddleware() if err != nil { panic(err) } return mw } // ToMiddleware converts Config to middleware. func (config ContextTimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if config.Timeout == 0 { return nil, errors.New("timeout must be set") } if config.Skipper == nil { config.Skipper = DefaultSkipper } if config.ErrorHandler == nil { config.ErrorHandler = func(err error, c echo.Context) error { if err != nil && errors.Is(err, context.DeadlineExceeded) { return echo.ErrServiceUnavailable.WithInternal(err) } return err } } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } timeoutContext, cancel := context.WithTimeout(c.Request().Context(), config.Timeout) defer cancel() c.SetRequest(c.Request().WithContext(timeoutContext)) if err := next(c); err != nil { return config.ErrorHandler(err, c) } return nil } }, nil } golang-github-labstack-echo-4.11.1/middleware/context_timeout_test.go000066400000000000000000000140441445647333300257770ustar00rootroot00000000000000package middleware import ( "context" "errors" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestContextTimeoutSkipper(t *testing.T) { t.Parallel() m := ContextTimeoutWithConfig(ContextTimeoutConfig{ Skipper: func(context echo.Context) bool { return true }, Timeout: 10 * time.Millisecond, }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) err := m(func(c echo.Context) error { if err := sleepWithContext(c.Request().Context(), time.Duration(20*time.Millisecond)); err != nil { return err } return errors.New("response from handler") })(c) // if not skipped we would have not returned error due context timeout logic assert.EqualError(t, err, "response from handler") } func TestContextTimeoutWithTimeout0(t *testing.T) { t.Parallel() assert.Panics(t, func() { ContextTimeout(time.Duration(0)) }) } func TestContextTimeoutErrorOutInHandler(t *testing.T) { t.Parallel() m := ContextTimeoutWithConfig(ContextTimeoutConfig{ // Timeout has to be defined or the whole flow for timeout middleware will be skipped Timeout: 10 * time.Millisecond, }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) rec.Code = 1 // we want to be sure that even 200 will not be sent err := m(func(c echo.Context) error { // this error must not be written to the client response. Middlewares upstream of timeout middleware must be able // to handle returned error and this can be done only then handler has not yet committed (written status code) // the response. return echo.NewHTTPError(http.StatusTeapot, "err") })(c) assert.Error(t, err) assert.EqualError(t, err, "code=418, message=err") assert.Equal(t, 1, rec.Code) assert.Equal(t, "", rec.Body.String()) } func TestContextTimeoutSuccessfulRequest(t *testing.T) { t.Parallel() m := ContextTimeoutWithConfig(ContextTimeoutConfig{ // Timeout has to be defined or the whole flow for timeout middleware will be skipped Timeout: 10 * time.Millisecond, }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) err := m(func(c echo.Context) error { return c.JSON(http.StatusCreated, map[string]string{"data": "ok"}) })(c) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, "{\"data\":\"ok\"}\n", rec.Body.String()) } func TestContextTimeoutTestRequestClone(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodPost, "/uri?query=value", strings.NewReader(url.Values{"form": {"value"}}.Encode())) req.AddCookie(&http.Cookie{Name: "cookie", Value: "value"}) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() m := ContextTimeoutWithConfig(ContextTimeoutConfig{ // Timeout has to be defined or the whole flow for timeout middleware will be skipped Timeout: 1 * time.Second, }) e := echo.New() c := e.NewContext(req, rec) err := m(func(c echo.Context) error { // Cookie test cookie, err := c.Request().Cookie("cookie") if assert.NoError(t, err) { assert.EqualValues(t, "cookie", cookie.Name) assert.EqualValues(t, "value", cookie.Value) } // Form values if assert.NoError(t, c.Request().ParseForm()) { assert.EqualValues(t, "value", c.Request().FormValue("form")) } // Query string assert.EqualValues(t, "value", c.Request().URL.Query()["query"][0]) return nil })(c) assert.NoError(t, err) } func TestContextTimeoutWithDefaultErrorMessage(t *testing.T) { t.Parallel() timeout := 10 * time.Millisecond m := ContextTimeoutWithConfig(ContextTimeoutConfig{ Timeout: timeout, }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) err := m(func(c echo.Context) error { if err := sleepWithContext(c.Request().Context(), time.Duration(80*time.Millisecond)); err != nil { return err } return c.String(http.StatusOK, "Hello, World!") })(c) assert.IsType(t, &echo.HTTPError{}, err) assert.Error(t, err) assert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code) assert.Equal(t, "Service Unavailable", err.(*echo.HTTPError).Message) } func TestContextTimeoutCanHandleContextDeadlineOnNextHandler(t *testing.T) { t.Parallel() timeoutErrorHandler := func(err error, c echo.Context) error { if err != nil { if errors.Is(err, context.DeadlineExceeded) { return &echo.HTTPError{ Code: http.StatusServiceUnavailable, Message: "Timeout! change me", } } return err } return nil } timeout := 50 * time.Millisecond m := ContextTimeoutWithConfig(ContextTimeoutConfig{ Timeout: timeout, ErrorHandler: timeoutErrorHandler, }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) err := m(func(c echo.Context) error { // NOTE: Very short periods are not reliable for tests due to Go routine scheduling and the unpredictable order // for 1) request and 2) time goroutine. For most OS this works as expected, but MacOS seems most flaky. if err := sleepWithContext(c.Request().Context(), 100*time.Millisecond); err != nil { return err } // The Request Context should have a Deadline set by http.ContextTimeoutHandler if _, ok := c.Request().Context().Deadline(); !ok { assert.Fail(t, "No timeout set on Request Context") } return c.String(http.StatusOK, "Hello, World!") })(c) assert.IsType(t, &echo.HTTPError{}, err) assert.Error(t, err) assert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code) assert.Equal(t, "Timeout! change me", err.(*echo.HTTPError).Message) } func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) defer func() { _ = timer.Stop() }() select { case <-ctx.Done(): return context.DeadlineExceeded case <-timer.C: return nil } } golang-github-labstack-echo-4.11.1/middleware/cors.go000066400000000000000000000256461445647333300224660ustar00rootroot00000000000000package middleware import ( "net/http" "regexp" "strconv" "strings" "github.com/labstack/echo/v4" ) type ( // CORSConfig defines the config for CORS middleware. CORSConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // AllowOrigins determines the value of the Access-Control-Allow-Origin // response header. This header defines a list of origins that may access the // resource. The wildcard characters '*' and '?' are supported and are // converted to regex fragments '.*' and '.' accordingly. // // Security: use extreme caution when handling the origin, and carefully // validate any logic. Remember that attackers may register hostile domain names. // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // // Optional. Default value []string{"*"}. // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin AllowOrigins []string `yaml:"allow_origins"` // AllowOriginFunc is a custom function to validate the origin. It takes the // origin as an argument and returns true if allowed or false otherwise. If // an error is returned, it is returned by the handler. If this option is // set, AllowOrigins is ignored. // // Security: use extreme caution when handling the origin, and carefully // validate any logic. Remember that attackers may register hostile domain names. // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // // Optional. AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"` // AllowMethods determines the value of the Access-Control-Allow-Methods // response header. This header specified the list of methods allowed when // accessing the resource. This is used in response to a preflight request. // // Optional. Default value DefaultCORSConfig.AllowMethods. // If `allowMethods` is left empty, this middleware will fill for preflight // request `Access-Control-Allow-Methods` header value // from `Allow` header that echo.Router set into context. // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods AllowMethods []string `yaml:"allow_methods"` // AllowHeaders determines the value of the Access-Control-Allow-Headers // response header. This header is used in response to a preflight request to // indicate which HTTP headers can be used when making the actual request. // // Optional. Default value []string{}. // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers AllowHeaders []string `yaml:"allow_headers"` // AllowCredentials determines the value of the // Access-Control-Allow-Credentials response header. This header indicates // whether or not the response to the request can be exposed when the // credentials mode (Request.credentials) is true. When used as part of a // response to a preflight request, this indicates whether or not the actual // request can be made using credentials. See also // [MDN: Access-Control-Allow-Credentials]. // // Optional. Default value false, in which case the header is not set. // // Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`. // See "Exploiting CORS misconfigurations for Bitcoins and bounties", // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials AllowCredentials bool `yaml:"allow_credentials"` // UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials // flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header. // // This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties) // attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject. // // Optional. Default value is false. UnsafeWildcardOriginWithAllowCredentials bool `yaml:"unsafe_wildcard_origin_with_allow_credentials"` // ExposeHeaders determines the value of Access-Control-Expose-Headers, which // defines a list of headers that clients are allowed to access. // // Optional. Default value []string{}, in which case the header is not set. // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header ExposeHeaders []string `yaml:"expose_headers"` // MaxAge determines the value of the Access-Control-Max-Age response header. // This header indicates how long (in seconds) the results of a preflight // request can be cached. // // Optional. Default value 0. The header is set only if MaxAge > 0. // // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age MaxAge int `yaml:"max_age"` } ) var ( // DefaultCORSConfig is the default CORS middleware config. DefaultCORSConfig = CORSConfig{ Skipper: DefaultSkipper, AllowOrigins: []string{"*"}, AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, } ) // CORS returns a Cross-Origin Resource Sharing (CORS) middleware. // See also [MDN: Cross-Origin Resource Sharing (CORS)]. // // Security: Poorly configured CORS can compromise security because it allows // relaxation of the browser's Same-Origin policy. See [Exploiting CORS // misconfigurations for Bitcoins and bounties] and [Portswigger: Cross-origin // resource sharing (CORS)] for more details. // // [MDN: Cross-Origin Resource Sharing (CORS)]: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS // [Exploiting CORS misconfigurations for Bitcoins and bounties]: https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // [Portswigger: Cross-origin resource sharing (CORS)]: https://portswigger.net/web-security/cors func CORS() echo.MiddlewareFunc { return CORSWithConfig(DefaultCORSConfig) } // CORSWithConfig returns a CORS middleware with config. // See: [CORS]. func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultCORSConfig.Skipper } if len(config.AllowOrigins) == 0 { config.AllowOrigins = DefaultCORSConfig.AllowOrigins } hasCustomAllowMethods := true if len(config.AllowMethods) == 0 { hasCustomAllowMethods = false config.AllowMethods = DefaultCORSConfig.AllowMethods } allowOriginPatterns := []string{} for _, origin := range config.AllowOrigins { pattern := regexp.QuoteMeta(origin) pattern = strings.ReplaceAll(pattern, "\\*", ".*") pattern = strings.ReplaceAll(pattern, "\\?", ".") pattern = "^" + pattern + "$" allowOriginPatterns = append(allowOriginPatterns, pattern) } allowMethods := strings.Join(config.AllowMethods, ",") allowHeaders := strings.Join(config.AllowHeaders, ",") exposeHeaders := strings.Join(config.ExposeHeaders, ",") maxAge := strconv.Itoa(config.MaxAge) return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() res := c.Response() origin := req.Header.Get(echo.HeaderOrigin) allowOrigin := "" res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) // Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method, // Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request // For simplicity we just consider method type and later `Origin` header. preflight := req.Method == http.MethodOptions // Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware // as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth // middlewares by calling next(c). // But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default // handler does. routerAllowMethods := "" if preflight { tmpAllowMethods, ok := c.Get(echo.ContextKeyHeaderAllow).(string) if ok && tmpAllowMethods != "" { routerAllowMethods = tmpAllowMethods c.Response().Header().Set(echo.HeaderAllow, routerAllowMethods) } } // No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain if origin == "" { if !preflight { return next(c) } return c.NoContent(http.StatusNoContent) } if config.AllowOriginFunc != nil { allowed, err := config.AllowOriginFunc(origin) if err != nil { return err } if allowed { allowOrigin = origin } } else { // Check allowed origins for _, o := range config.AllowOrigins { if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials { allowOrigin = origin break } if o == "*" || o == origin { allowOrigin = o break } if matchSubdomain(origin, o) { allowOrigin = origin break } } checkPatterns := false if allowOrigin == "" { // to avoid regex cost by invalid (long) domains (253 is domain name max limit) if len(origin) <= (253+3+5) && strings.Contains(origin, "://") { checkPatterns = true } } if checkPatterns { for _, re := range allowOriginPatterns { if match, _ := regexp.MatchString(re, origin); match { allowOrigin = origin break } } } } // Origin not allowed if allowOrigin == "" { if !preflight { return next(c) } return c.NoContent(http.StatusNoContent) } res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin) if config.AllowCredentials { res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") } // Simple request if !preflight { if exposeHeaders != "" { res.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders) } return next(c) } // Preflight request res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod) res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders) if !hasCustomAllowMethods && routerAllowMethods != "" { res.Header().Set(echo.HeaderAccessControlAllowMethods, routerAllowMethods) } else { res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods) } if allowHeaders != "" { res.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders) } else { h := req.Header.Get(echo.HeaderAccessControlRequestHeaders) if h != "" { res.Header().Set(echo.HeaderAccessControlAllowHeaders, h) } } if config.MaxAge > 0 { res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge) } return c.NoContent(http.StatusNoContent) } } } golang-github-labstack-echo-4.11.1/middleware/cors_test.go000066400000000000000000000466711445647333300235260ustar00rootroot00000000000000package middleware import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestCORS(t *testing.T) { var testCases = []struct { name string givenMW echo.MiddlewareFunc whenMethod string whenHeaders map[string]string expectHeaders map[string]string notExpectHeaders map[string]string }{ { name: "ok, wildcard origin", whenHeaders: map[string]string{echo.HeaderOrigin: "localhost"}, expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "*"}, }, { name: "ok, wildcard AllowedOrigin with no Origin header in request", notExpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: ""}, }, { name: "ok, specific AllowOrigins and AllowCredentials", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"localhost"}, AllowCredentials: true, MaxAge: 3600, }), whenHeaders: map[string]string{echo.HeaderOrigin: "localhost"}, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "localhost", echo.HeaderAccessControlAllowCredentials: "true", }, }, { name: "ok, preflight request with matching origin for `AllowOrigins`", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"localhost"}, AllowCredentials: true, MaxAge: 3600, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{ echo.HeaderOrigin: "localhost", echo.HeaderContentType: echo.MIMEApplicationJSON, }, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "localhost", echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", echo.HeaderAccessControlAllowCredentials: "true", echo.HeaderAccessControlMaxAge: "3600", }, }, { name: "ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` true", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, AllowCredentials: true, MaxAge: 3600, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{ echo.HeaderOrigin: "localhost", echo.HeaderContentType: echo.MIMEApplicationJSON, }, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "*", // Note: browsers will ignore and complain about responses having `*` echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", echo.HeaderAccessControlAllowCredentials: "true", echo.HeaderAccessControlMaxAge: "3600", }, }, { name: "ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` false", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, AllowCredentials: false, // important for this testcase MaxAge: 3600, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{ echo.HeaderOrigin: "localhost", echo.HeaderContentType: echo.MIMEApplicationJSON, }, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "*", echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", echo.HeaderAccessControlMaxAge: "3600", }, notExpectHeaders: map[string]string{ echo.HeaderAccessControlAllowCredentials: "", }, }, { name: "ok, INSECURE preflight request with wildcard `AllowOrigins` and `AllowCredentials` true", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, AllowCredentials: true, UnsafeWildcardOriginWithAllowCredentials: true, // important for this testcase MaxAge: 3600, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{ echo.HeaderOrigin: "localhost", echo.HeaderContentType: echo.MIMEApplicationJSON, }, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "localhost", // This could end up as cross-origin attack echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", echo.HeaderAccessControlAllowCredentials: "true", echo.HeaderAccessControlMaxAge: "3600", }, }, { name: "ok, preflight request with Access-Control-Request-Headers", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{ echo.HeaderOrigin: "localhost", echo.HeaderContentType: echo.MIMEApplicationJSON, echo.HeaderAccessControlRequestHeaders: "Special-Request-Header", }, expectHeaders: map[string]string{ echo.HeaderAccessControlAllowOrigin: "*", echo.HeaderAccessControlAllowHeaders: "Special-Request-Header", echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", }, }, { name: "ok, preflight request with `AllowOrigins` which allow all subdomains aaa with *", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"http://*.example.com"}, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{echo.HeaderOrigin: "http://aaa.example.com"}, expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "http://aaa.example.com"}, }, { name: "ok, preflight request with `AllowOrigins` which allow all subdomains bbb with *", givenMW: CORSWithConfig(CORSConfig{ AllowOrigins: []string{"http://*.example.com"}, }), whenMethod: http.MethodOptions, whenHeaders: map[string]string{echo.HeaderOrigin: "http://bbb.example.com"}, expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "http://bbb.example.com"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() mw := CORS() if tc.givenMW != nil { mw = tc.givenMW } h := mw(func(c echo.Context) error { return nil }) method := http.MethodGet if tc.whenMethod != "" { method = tc.whenMethod } req := httptest.NewRequest(method, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) for k, v := range tc.whenHeaders { req.Header.Set(k, v) } err := h(c) assert.NoError(t, err) header := rec.Header() for k, v := range tc.expectHeaders { assert.Equal(t, v, header.Get(k), "header: `%v` should be `%v`", k, v) } for k, v := range tc.notExpectHeaders { if v == "" { assert.Len(t, header.Values(k), 0, "header: `%v` should not be set", k) } else { assert.NotEqual(t, v, header.Get(k), "header: `%v` should not be `%v`", k, v) } } }) } } func Test_allowOriginScheme(t *testing.T) { tests := []struct { domain, pattern string expected bool }{ { domain: "http://example.com", pattern: "http://example.com", expected: true, }, { domain: "https://example.com", pattern: "https://example.com", expected: true, }, { domain: "http://example.com", pattern: "https://example.com", expected: false, }, { domain: "https://example.com", pattern: "http://example.com", expected: false, }, } e := echo.New() for _, tt := range tests { req := httptest.NewRequest(http.MethodOptions, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, tt.domain) cors := CORSWithConfig(CORSConfig{ AllowOrigins: []string{tt.pattern}, }) h := cors(echo.NotFoundHandler) h(c) if tt.expected { assert.Equal(t, tt.domain, rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) } else { assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin) } } } func Test_allowOriginSubdomain(t *testing.T) { tests := []struct { domain, pattern string expected bool }{ { domain: "http://aaa.example.com", pattern: "http://*.example.com", expected: true, }, { domain: "http://bbb.aaa.example.com", pattern: "http://*.example.com", expected: true, }, { domain: "http://bbb.aaa.example.com", pattern: "http://*.aaa.example.com", expected: true, }, { domain: "http://aaa.example.com:8080", pattern: "http://*.example.com:8080", expected: true, }, { domain: "http://fuga.hoge.com", pattern: "http://*.example.com", expected: false, }, { domain: "http://ccc.bbb.example.com", pattern: "http://*.aaa.example.com", expected: false, }, { domain: `http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com`, pattern: "http://*.example.com", expected: false, }, { domain: `http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com`, pattern: "http://*.example.com", expected: false, }, { domain: "http://ccc.bbb.example.com", pattern: "http://example.com", expected: false, }, { domain: "https://prod-preview--aaa.bbb.com", pattern: "https://*--aaa.bbb.com", expected: true, }, { domain: "http://ccc.bbb.example.com", pattern: "http://*.example.com", expected: true, }, { domain: "http://ccc.bbb.example.com", pattern: "http://foo.[a-z]*.example.com", expected: false, }, } e := echo.New() for _, tt := range tests { req := httptest.NewRequest(http.MethodOptions, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, tt.domain) cors := CORSWithConfig(CORSConfig{ AllowOrigins: []string{tt.pattern}, }) h := cors(echo.NotFoundHandler) h(c) if tt.expected { assert.Equal(t, tt.domain, rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) } else { assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin) } } } func TestCORSWithConfig_AllowMethods(t *testing.T) { var testCases = []struct { name string allowOrigins []string allowContextKey string whenOrigin string whenAllowMethods []string expectAllow string expectAccessControlAllowMethods string }{ { name: "custom AllowMethods, preflight, no origin, sets only allow header from context key", allowContextKey: "OPTIONS, GET", whenAllowMethods: []string{http.MethodGet, http.MethodHead}, whenOrigin: "", expectAllow: "OPTIONS, GET", }, { name: "default AllowMethods, preflight, no origin, no allow header in context key and in response", allowContextKey: "", whenAllowMethods: nil, whenOrigin: "", expectAllow: "", }, { name: "custom AllowMethods, preflight, existing origin, sets both headers different values", allowContextKey: "OPTIONS, GET", whenAllowMethods: []string{http.MethodGet, http.MethodHead}, whenOrigin: "http://google.com", expectAllow: "OPTIONS, GET", expectAccessControlAllowMethods: "GET,HEAD", }, { name: "default AllowMethods, preflight, existing origin, sets both headers", allowContextKey: "OPTIONS, GET", whenAllowMethods: nil, whenOrigin: "http://google.com", expectAllow: "OPTIONS, GET", expectAccessControlAllowMethods: "OPTIONS, GET", }, { name: "default AllowMethods, preflight, existing origin, no allows, sets only CORS allow methods", allowContextKey: "", whenAllowMethods: nil, whenOrigin: "http://google.com", expectAllow: "", expectAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.GET("/test", func(c echo.Context) error { return c.String(http.StatusOK, "OK") }) cors := CORSWithConfig(CORSConfig{ AllowOrigins: tc.allowOrigins, AllowMethods: tc.whenAllowMethods, }) req := httptest.NewRequest(http.MethodOptions, "/test", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, tc.whenOrigin) if tc.allowContextKey != "" { c.Set(echo.ContextKeyHeaderAllow, tc.allowContextKey) } h := cors(echo.NotFoundHandler) h(c) assert.Equal(t, tc.expectAllow, rec.Header().Get(echo.HeaderAllow)) assert.Equal(t, tc.expectAccessControlAllowMethods, rec.Header().Get(echo.HeaderAccessControlAllowMethods)) }) } } func TestCorsHeaders(t *testing.T) { tests := []struct { name string originDomain string method string allowedOrigin string expected bool expectStatus int expectAllowHeader string }{ { name: "non-preflight request, allow any origin, missing origin header = no CORS logic done", originDomain: "", allowedOrigin: "*", method: http.MethodGet, expected: false, expectStatus: http.StatusOK, }, { name: "non-preflight request, allow any origin, specific origin domain", originDomain: "http://example.com", allowedOrigin: "*", method: http.MethodGet, expected: true, expectStatus: http.StatusOK, }, { name: "non-preflight request, allow specific origin, missing origin header = no CORS logic done", originDomain: "", // Request does not have Origin header allowedOrigin: "http://example.com", method: http.MethodGet, expected: false, expectStatus: http.StatusOK, }, { name: "non-preflight request, allow specific origin, different origin header = CORS logic failure", originDomain: "http://bar.com", allowedOrigin: "http://example.com", method: http.MethodGet, expected: false, expectStatus: http.StatusOK, }, { name: "non-preflight request, allow specific origin, matching origin header = CORS logic done", originDomain: "http://example.com", allowedOrigin: "http://example.com", method: http.MethodGet, expected: true, expectStatus: http.StatusOK, }, { name: "preflight, allow any origin, missing origin header = no CORS logic done", originDomain: "", // Request does not have Origin header allowedOrigin: "*", method: http.MethodOptions, expected: false, expectStatus: http.StatusNoContent, expectAllowHeader: "OPTIONS, GET, POST", }, { name: "preflight, allow any origin, existing origin header = CORS logic done", originDomain: "http://example.com", allowedOrigin: "*", method: http.MethodOptions, expected: true, expectStatus: http.StatusNoContent, expectAllowHeader: "OPTIONS, GET, POST", }, { name: "preflight, allow any origin, missing origin header = no CORS logic done", originDomain: "", // Request does not have Origin header allowedOrigin: "http://example.com", method: http.MethodOptions, expected: false, expectStatus: http.StatusNoContent, expectAllowHeader: "OPTIONS, GET, POST", }, { name: "preflight, allow specific origin, different origin header = no CORS logic done", originDomain: "http://bar.com", allowedOrigin: "http://example.com", method: http.MethodOptions, expected: false, expectStatus: http.StatusNoContent, expectAllowHeader: "OPTIONS, GET, POST", }, { name: "preflight, allow specific origin, matching origin header = CORS logic done", originDomain: "http://example.com", allowedOrigin: "http://example.com", method: http.MethodOptions, expected: true, expectStatus: http.StatusNoContent, expectAllowHeader: "OPTIONS, GET, POST", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.Use(CORSWithConfig(CORSConfig{ AllowOrigins: []string{tc.allowedOrigin}, //AllowCredentials: true, //MaxAge: 3600, })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "OK") }) e.POST("/", func(c echo.Context) error { return c.String(http.StatusCreated, "OK") }) req := httptest.NewRequest(tc.method, "/", nil) rec := httptest.NewRecorder() if tc.originDomain != "" { req.Header.Set(echo.HeaderOrigin, tc.originDomain) } // we run through whole Echo handler chain to see how CORS works with Router OPTIONS handler e.ServeHTTP(rec, req) assert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary)) assert.Equal(t, tc.expectAllowHeader, rec.Header().Get(echo.HeaderAllow)) assert.Equal(t, tc.expectStatus, rec.Code) expectedAllowOrigin := "" if tc.allowedOrigin == "*" { expectedAllowOrigin = "*" } else { expectedAllowOrigin = tc.originDomain } switch { case tc.expected && tc.method == http.MethodOptions: assert.Contains(t, rec.Header(), echo.HeaderAccessControlAllowMethods) assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.Equal(t, 3, len(rec.Header()[echo.HeaderVary])) case tc.expected && tc.method == http.MethodGet: assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin default: assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin) assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin } }) } } func Test_allowOriginFunc(t *testing.T) { returnTrue := func(origin string) (bool, error) { return true, nil } returnFalse := func(origin string) (bool, error) { return false, nil } returnError := func(origin string) (bool, error) { return true, errors.New("this is a test error") } allowOriginFuncs := []func(origin string) (bool, error){ returnTrue, returnFalse, returnError, } const origin = "http://example.com" e := echo.New() for _, allowOriginFunc := range allowOriginFuncs { req := httptest.NewRequest(http.MethodOptions, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, origin) cors := CORSWithConfig(CORSConfig{ AllowOriginFunc: allowOriginFunc, }) h := cors(echo.NotFoundHandler) err := h(c) expected, expectedErr := allowOriginFunc(origin) if expectedErr != nil { assert.Equal(t, expectedErr, err) assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) continue } if expected { assert.Equal(t, origin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) } else { assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) } } } golang-github-labstack-echo-4.11.1/middleware/csrf.go000066400000000000000000000147251445647333300224510ustar00rootroot00000000000000package middleware import ( "crypto/subtle" "net/http" "time" "github.com/labstack/echo/v4" "github.com/labstack/gommon/random" ) type ( // CSRFConfig defines the config for CSRF middleware. CSRFConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // TokenLength is the length of the generated token. TokenLength uint8 `yaml:"token_length"` // Optional. Default value 32. // TokenLookup is a string in the form of ":" or ":,:" that is used // to extract token from the request. // Optional. Default value "header:X-CSRF-Token". // Possible values: // - "header:" or "header::" // - "query:" // - "form:" // Multiple sources example: // - "header:X-CSRF-Token,query:csrf" TokenLookup string `yaml:"token_lookup"` // Context key to store generated CSRF token into context. // Optional. Default value "csrf". ContextKey string `yaml:"context_key"` // Name of the CSRF cookie. This cookie will store CSRF token. // Optional. Default value "csrf". CookieName string `yaml:"cookie_name"` // Domain of the CSRF cookie. // Optional. Default value none. CookieDomain string `yaml:"cookie_domain"` // Path of the CSRF cookie. // Optional. Default value none. CookiePath string `yaml:"cookie_path"` // Max age (in seconds) of the CSRF cookie. // Optional. Default value 86400 (24hr). CookieMaxAge int `yaml:"cookie_max_age"` // Indicates if CSRF cookie is secure. // Optional. Default value false. CookieSecure bool `yaml:"cookie_secure"` // Indicates if CSRF cookie is HTTP only. // Optional. Default value false. CookieHTTPOnly bool `yaml:"cookie_http_only"` // Indicates SameSite mode of the CSRF cookie. // Optional. Default value SameSiteDefaultMode. CookieSameSite http.SameSite `yaml:"cookie_same_site"` // ErrorHandler defines a function which is executed for returning custom errors. ErrorHandler CSRFErrorHandler } // CSRFErrorHandler is a function which is executed for creating custom errors. CSRFErrorHandler func(err error, c echo.Context) error ) // ErrCSRFInvalid is returned when CSRF check fails var ErrCSRFInvalid = echo.NewHTTPError(http.StatusForbidden, "invalid csrf token") var ( // DefaultCSRFConfig is the default CSRF middleware config. DefaultCSRFConfig = CSRFConfig{ Skipper: DefaultSkipper, TokenLength: 32, TokenLookup: "header:" + echo.HeaderXCSRFToken, ContextKey: "csrf", CookieName: "_csrf", CookieMaxAge: 86400, CookieSameSite: http.SameSiteDefaultMode, } ) // CSRF returns a Cross-Site Request Forgery (CSRF) middleware. // See: https://en.wikipedia.org/wiki/Cross-site_request_forgery func CSRF() echo.MiddlewareFunc { c := DefaultCSRFConfig return CSRFWithConfig(c) } // CSRFWithConfig returns a CSRF middleware with config. // See `CSRF()`. func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultCSRFConfig.Skipper } if config.TokenLength == 0 { config.TokenLength = DefaultCSRFConfig.TokenLength } if config.TokenLookup == "" { config.TokenLookup = DefaultCSRFConfig.TokenLookup } if config.ContextKey == "" { config.ContextKey = DefaultCSRFConfig.ContextKey } if config.CookieName == "" { config.CookieName = DefaultCSRFConfig.CookieName } if config.CookieMaxAge == 0 { config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge } if config.CookieSameSite == http.SameSiteNoneMode { config.CookieSecure = true } extractors, cErr := CreateExtractors(config.TokenLookup) if cErr != nil { panic(cErr) } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } token := "" if k, err := c.Cookie(config.CookieName); err != nil { token = random.String(config.TokenLength) // Generate token } else { token = k.Value // Reuse token } switch c.Request().Method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace: default: // Validate token only for requests which are not defined as 'safe' by RFC7231 var lastExtractorErr error var lastTokenErr error outer: for _, extractor := range extractors { clientTokens, err := extractor(c) if err != nil { lastExtractorErr = err continue } for _, clientToken := range clientTokens { if validateCSRFToken(token, clientToken) { lastTokenErr = nil lastExtractorErr = nil break outer } lastTokenErr = ErrCSRFInvalid } } var finalErr error if lastTokenErr != nil { finalErr = lastTokenErr } else if lastExtractorErr != nil { // ugly part to preserve backwards compatible errors. someone could rely on them if lastExtractorErr == errQueryExtractorValueMissing { lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, "missing csrf token in the query string") } else if lastExtractorErr == errFormExtractorValueMissing { lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, "missing csrf token in the form parameter") } else if lastExtractorErr == errHeaderExtractorValueMissing { lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, "missing csrf token in request header") } else { lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, lastExtractorErr.Error()) } finalErr = lastExtractorErr } if finalErr != nil { if config.ErrorHandler != nil { return config.ErrorHandler(finalErr, c) } return finalErr } } // Set CSRF cookie cookie := new(http.Cookie) cookie.Name = config.CookieName cookie.Value = token if config.CookiePath != "" { cookie.Path = config.CookiePath } if config.CookieDomain != "" { cookie.Domain = config.CookieDomain } if config.CookieSameSite != http.SameSiteDefaultMode { cookie.SameSite = config.CookieSameSite } cookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second) cookie.Secure = config.CookieSecure cookie.HttpOnly = config.CookieHTTPOnly c.SetCookie(cookie) // Store token in the context c.Set(config.ContextKey, token) // Protect clients from caching the response c.Response().Header().Add(echo.HeaderVary, echo.HeaderCookie) return next(c) } } } func validateCSRFToken(token, clientToken string) bool { return subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1 } golang-github-labstack-echo-4.11.1/middleware/csrf_test.go000066400000000000000000000233711445647333300235050ustar00rootroot00000000000000package middleware import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/labstack/echo/v4" "github.com/labstack/gommon/random" "github.com/stretchr/testify/assert" ) func TestCSRF_tokenExtractors(t *testing.T) { var testCases = []struct { name string whenTokenLookup string whenCookieName string givenCSRFCookie string givenMethod string givenQueryTokens map[string][]string givenFormTokens map[string][]string givenHeaderTokens map[string][]string expectError string }{ { name: "ok, multiple token lookups sources, succeeds on last one", whenTokenLookup: "header:X-CSRF-Token,form:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPost, givenHeaderTokens: map[string][]string{ echo.HeaderXCSRFToken: {"invalid_token"}, }, givenFormTokens: map[string][]string{ "csrf": {"token"}, }, }, { name: "ok, token from POST form", whenTokenLookup: "form:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPost, givenFormTokens: map[string][]string{ "csrf": {"token"}, }, }, { name: "ok, token from POST form, second token passes", whenTokenLookup: "form:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPost, givenFormTokens: map[string][]string{ "csrf": {"invalid", "token"}, }, }, { name: "nok, invalid token from POST form", whenTokenLookup: "form:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPost, givenFormTokens: map[string][]string{ "csrf": {"invalid_token"}, }, expectError: "code=403, message=invalid csrf token", }, { name: "nok, missing token from POST form", whenTokenLookup: "form:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPost, givenFormTokens: map[string][]string{}, expectError: "code=400, message=missing csrf token in the form parameter", }, { name: "ok, token from POST header", whenTokenLookup: "", // will use defaults givenCSRFCookie: "token", givenMethod: http.MethodPost, givenHeaderTokens: map[string][]string{ echo.HeaderXCSRFToken: {"token"}, }, }, { name: "ok, token from POST header, second token passes", whenTokenLookup: "header:" + echo.HeaderXCSRFToken, givenCSRFCookie: "token", givenMethod: http.MethodPost, givenHeaderTokens: map[string][]string{ echo.HeaderXCSRFToken: {"invalid", "token"}, }, }, { name: "nok, invalid token from POST header", whenTokenLookup: "header:" + echo.HeaderXCSRFToken, givenCSRFCookie: "token", givenMethod: http.MethodPost, givenHeaderTokens: map[string][]string{ echo.HeaderXCSRFToken: {"invalid_token"}, }, expectError: "code=403, message=invalid csrf token", }, { name: "nok, missing token from POST header", whenTokenLookup: "header:" + echo.HeaderXCSRFToken, givenCSRFCookie: "token", givenMethod: http.MethodPost, givenHeaderTokens: map[string][]string{}, expectError: "code=400, message=missing csrf token in request header", }, { name: "ok, token from PUT query param", whenTokenLookup: "query:csrf-param", givenCSRFCookie: "token", givenMethod: http.MethodPut, givenQueryTokens: map[string][]string{ "csrf-param": {"token"}, }, }, { name: "ok, token from PUT query form, second token passes", whenTokenLookup: "query:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPut, givenQueryTokens: map[string][]string{ "csrf": {"invalid", "token"}, }, }, { name: "nok, invalid token from PUT query form", whenTokenLookup: "query:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPut, givenQueryTokens: map[string][]string{ "csrf": {"invalid_token"}, }, expectError: "code=403, message=invalid csrf token", }, { name: "nok, missing token from PUT query form", whenTokenLookup: "query:csrf", givenCSRFCookie: "token", givenMethod: http.MethodPut, givenQueryTokens: map[string][]string{}, expectError: "code=400, message=missing csrf token in the query string", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() q := make(url.Values) for queryParam, values := range tc.givenQueryTokens { for _, v := range values { q.Add(queryParam, v) } } f := make(url.Values) for formKey, values := range tc.givenFormTokens { for _, v := range values { f.Add(formKey, v) } } var req *http.Request switch tc.givenMethod { case http.MethodGet: req = httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) case http.MethodPost, http.MethodPut: req = httptest.NewRequest(http.MethodPost, "/?"+q.Encode(), strings.NewReader(f.Encode())) req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) } for header, values := range tc.givenHeaderTokens { for _, v := range values { req.Header.Add(header, v) } } if tc.givenCSRFCookie != "" { req.Header.Set(echo.HeaderCookie, "_csrf="+tc.givenCSRFCookie) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ TokenLookup: tc.whenTokenLookup, CookieName: tc.whenCookieName, }) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) err := h(c) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestCSRF(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRF() h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) // Generate CSRF token h(c) assert.Contains(t, rec.Header().Get(echo.HeaderSetCookie), "_csrf") // Without CSRF cookie req = httptest.NewRequest(http.MethodPost, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) assert.Error(t, h(c)) // Empty/invalid CSRF token req = httptest.NewRequest(http.MethodPost, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) req.Header.Set(echo.HeaderXCSRFToken, "") assert.Error(t, h(c)) // Valid CSRF token token := random.String(32) req.Header.Set(echo.HeaderCookie, "_csrf="+token) req.Header.Set(echo.HeaderXCSRFToken, token) if assert.NoError(t, h(c)) { assert.Equal(t, http.StatusOK, rec.Code) } } func TestCSRFSetSameSiteMode(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ CookieSameSite: http.SameSiteStrictMode, }) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) r := h(c) assert.NoError(t, r) assert.Regexp(t, "SameSite=Strict", rec.Header()["Set-Cookie"]) } func TestCSRFWithoutSameSiteMode(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{}) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) r := h(c) assert.NoError(t, r) assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"]) } func TestCSRFWithSameSiteDefaultMode(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ CookieSameSite: http.SameSiteDefaultMode, }) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) r := h(c) assert.NoError(t, r) assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"]) } func TestCSRFWithSameSiteModeNone(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ CookieSameSite: http.SameSiteNoneMode, }) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) r := h(c) assert.NoError(t, r) assert.Regexp(t, "SameSite=None", rec.Header()["Set-Cookie"]) assert.Regexp(t, "Secure", rec.Header()["Set-Cookie"]) } func TestCSRFConfig_skipper(t *testing.T) { var testCases = []struct { name string whenSkip bool expectCookies int }{ { name: "do skip", whenSkip: true, expectCookies: 0, }, { name: "do not skip", whenSkip: false, expectCookies: 1, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ Skipper: func(c echo.Context) bool { return tc.whenSkip }, }) h := csrf(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) r := h(c) assert.NoError(t, r) cookie := rec.Header()["Set-Cookie"] assert.Len(t, cookie, tc.expectCookies) }) } } func TestCSRFErrorHandling(t *testing.T) { cfg := CSRFConfig{ ErrorHandler: func(err error, c echo.Context) error { return echo.NewHTTPError(http.StatusTeapot, "error_handler_executed") }, } e := echo.New() e.POST("/", func(c echo.Context) error { return c.String(http.StatusNotImplemented, "should not end up here") }) e.Use(CSRFWithConfig(cfg)) req := httptest.NewRequest(http.MethodPost, "/", nil) res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusTeapot, res.Code) assert.Equal(t, "{\"message\":\"error_handler_executed\"}\n", res.Body.String()) } golang-github-labstack-echo-4.11.1/middleware/decompress.go000066400000000000000000000050721445647333300236530ustar00rootroot00000000000000package middleware import ( "compress/gzip" "io" "net/http" "sync" "github.com/labstack/echo/v4" ) type ( // DecompressConfig defines the config for Decompress middleware. DecompressConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers GzipDecompressPool Decompressor } ) // GZIPEncoding content-encoding header if set to "gzip", decompress body contents. const GZIPEncoding string = "gzip" // Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers type Decompressor interface { gzipDecompressPool() sync.Pool } var ( //DefaultDecompressConfig defines the config for decompress middleware DefaultDecompressConfig = DecompressConfig{ Skipper: DefaultSkipper, GzipDecompressPool: &DefaultGzipDecompressPool{}, } ) // DefaultGzipDecompressPool is the default implementation of Decompressor interface type DefaultGzipDecompressPool struct { } func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool { return sync.Pool{New: func() interface{} { return new(gzip.Reader) }} } // Decompress decompresses request body based if content encoding type is set to "gzip" with default config func Decompress() echo.MiddlewareFunc { return DecompressWithConfig(DefaultDecompressConfig) } // DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultGzipConfig.Skipper } if config.GzipDecompressPool == nil { config.GzipDecompressPool = DefaultDecompressConfig.GzipDecompressPool } return func(next echo.HandlerFunc) echo.HandlerFunc { pool := config.GzipDecompressPool.gzipDecompressPool() return func(c echo.Context) error { if config.Skipper(c) { return next(c) } if c.Request().Header.Get(echo.HeaderContentEncoding) != GZIPEncoding { return next(c) } i := pool.Get() gr, ok := i.(*gzip.Reader) if !ok || gr == nil { return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error()) } defer pool.Put(gr) b := c.Request().Body defer b.Close() if err := gr.Reset(b); err != nil { if err == io.EOF { //ignore if body is empty return next(c) } return err } // only Close gzip reader if it was set to a proper gzip source otherwise it will panic on close. defer gr.Close() c.Request().Body = gr return next(c) } } } golang-github-labstack-echo-4.11.1/middleware/decompress_test.go000066400000000000000000000132451445647333300247130ustar00rootroot00000000000000package middleware import ( "bytes" "compress/gzip" "errors" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestDecompress(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test")) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // Skip if no Content-Encoding header h := Decompress()(func(c echo.Context) error { c.Response().Write([]byte("test")) // For Content-Type sniffing return nil }) h(c) assert.Equal(t, "test", rec.Body.String()) // Decompress body := `{"name": "echo"}` gz, _ := gzipString(body) req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h(c) assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) b, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.Equal(t, body, string(b)) } func TestDecompressDefaultConfig(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test")) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := DecompressWithConfig(DecompressConfig{})(func(c echo.Context) error { c.Response().Write([]byte("test")) // For Content-Type sniffing return nil }) h(c) assert.Equal(t, "test", rec.Body.String()) // Decompress body := `{"name": "echo"}` gz, _ := gzipString(body) req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h(c) assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) b, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.Equal(t, body, string(b)) } func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) { e := echo.New() body := `{"name":"echo"}` gz, _ := gzipString(body) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec := httptest.NewRecorder() e.NewContext(req, rec) e.ServeHTTP(rec, req) assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) b, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.NotEqual(t, b, body) assert.Equal(t, b, gz) } func TestDecompressNoContent(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Decompress()(func(c echo.Context) error { return c.NoContent(http.StatusNoContent) }) if assert.NoError(t, h(c)) { assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) assert.Empty(t, rec.Header().Get(echo.HeaderContentType)) assert.Equal(t, 0, len(rec.Body.Bytes())) } } func TestDecompressErrorReturned(t *testing.T) { e := echo.New() e.Use(Decompress()) e.GET("/", func(c echo.Context) error { return echo.ErrNotFound }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) } func TestDecompressSkipper(t *testing.T) { e := echo.New() e.Use(DecompressWithConfig(DecompressConfig{ Skipper: func(c echo.Context) bool { return c.Request().URL.Path == "/skip" }, })) body := `{"name": "echo"}` req := httptest.NewRequest(http.MethodPost, "/skip", strings.NewReader(body)) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec := httptest.NewRecorder() c := e.NewContext(req, rec) e.ServeHTTP(rec, req) assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8) reqBody, err := io.ReadAll(c.Request().Body) assert.NoError(t, err) assert.Equal(t, body, string(reqBody)) } type TestDecompressPoolWithError struct { } func (d *TestDecompressPoolWithError) gzipDecompressPool() sync.Pool { return sync.Pool{ New: func() interface{} { return errors.New("pool error") }, } } func TestDecompressPoolError(t *testing.T) { e := echo.New() e.Use(DecompressWithConfig(DecompressConfig{ Skipper: DefaultSkipper, GzipDecompressPool: &TestDecompressPoolWithError{}, })) body := `{"name": "echo"}` req := httptest.NewRequest(http.MethodPost, "/echo", strings.NewReader(body)) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) rec := httptest.NewRecorder() c := e.NewContext(req, rec) e.ServeHTTP(rec, req) assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) reqBody, err := io.ReadAll(c.Request().Body) assert.NoError(t, err) assert.Equal(t, body, string(reqBody)) assert.Equal(t, rec.Code, http.StatusInternalServerError) } func BenchmarkDecompress(b *testing.B) { e := echo.New() body := `{"name": "echo"}` gz, _ := gzipString(body) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) h := Decompress()(func(c echo.Context) error { c.Response().Write([]byte(body)) // For Content-Type sniffing return nil }) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { // Decompress rec := httptest.NewRecorder() c := e.NewContext(req, rec) h(c) } } func gzipString(body string) ([]byte, error) { var buf bytes.Buffer gz := gzip.NewWriter(&buf) _, err := gz.Write([]byte(body)) if err != nil { return nil, err } if err := gz.Close(); err != nil { return nil, err } return buf.Bytes(), nil } golang-github-labstack-echo-4.11.1/middleware/extractor.go000066400000000000000000000154701445647333300235250ustar00rootroot00000000000000package middleware import ( "errors" "fmt" "github.com/labstack/echo/v4" "net/textproto" "strings" ) const ( // extractorLimit is arbitrary number to limit values extractor can return. this limits possible resource exhaustion // attack vector extractorLimit = 20 ) var errHeaderExtractorValueMissing = errors.New("missing value in request header") var errHeaderExtractorValueInvalid = errors.New("invalid value in request header") var errQueryExtractorValueMissing = errors.New("missing value in the query string") var errParamExtractorValueMissing = errors.New("missing value in path params") var errCookieExtractorValueMissing = errors.New("missing value in cookies") var errFormExtractorValueMissing = errors.New("missing value in the form") // ValuesExtractor defines a function for extracting values (keys/tokens) from the given context. type ValuesExtractor func(c echo.Context) ([]string, error) // CreateExtractors creates ValuesExtractors from given lookups. // Lookups is a string in the form of ":" or ":,:" that is used // to extract key from the request. // Possible values: // - "header:" or "header::" // `` is argument value to cut/trim prefix of the extracted value. This is useful if header // value has static prefix like `Authorization: ` where part that we // want to cut is ` ` note the space at the end. // In case of basic authentication `Authorization: Basic ` prefix we want to remove is `Basic `. // - "query:" // - "param:" // - "form:" // - "cookie:" // // Multiple sources example: // - "header:Authorization,header:X-Api-Key" func CreateExtractors(lookups string) ([]ValuesExtractor, error) { return createExtractors(lookups, "") } func createExtractors(lookups string, authScheme string) ([]ValuesExtractor, error) { if lookups == "" { return nil, nil } sources := strings.Split(lookups, ",") var extractors = make([]ValuesExtractor, 0) for _, source := range sources { parts := strings.Split(source, ":") if len(parts) < 2 { return nil, fmt.Errorf("extractor source for lookup could not be split into needed parts: %v", source) } switch parts[0] { case "query": extractors = append(extractors, valuesFromQuery(parts[1])) case "param": extractors = append(extractors, valuesFromParam(parts[1])) case "cookie": extractors = append(extractors, valuesFromCookie(parts[1])) case "form": extractors = append(extractors, valuesFromForm(parts[1])) case "header": prefix := "" if len(parts) > 2 { prefix = parts[2] } else if authScheme != "" && parts[1] == echo.HeaderAuthorization { // backwards compatibility for JWT and KeyAuth: // * we only apply this fix to Authorization as header we use and uses prefixes like "Bearer " etc // * previously header extractor assumed that auth-scheme/prefix had a space as suffix we need to retain that // behaviour for default values and Authorization header. prefix = authScheme if !strings.HasSuffix(prefix, " ") { prefix += " " } } extractors = append(extractors, valuesFromHeader(parts[1], prefix)) } } return extractors, nil } // valuesFromHeader returns a functions that extracts values from the request header. // valuePrefix is parameter to remove first part (prefix) of the extracted value. This is useful if header value has static // prefix like `Authorization: ` where part that we want to remove is ` ` // note the space at the end. In case of basic authentication `Authorization: Basic ` prefix we want to remove // is `Basic `. In case of JWT tokens `Authorization: Bearer ` prefix is `Bearer `. // If prefix is left empty the whole value is returned. func valuesFromHeader(header string, valuePrefix string) ValuesExtractor { prefixLen := len(valuePrefix) // standard library parses http.Request header keys in canonical form but we may provide something else so fix this header = textproto.CanonicalMIMEHeaderKey(header) return func(c echo.Context) ([]string, error) { values := c.Request().Header.Values(header) if len(values) == 0 { return nil, errHeaderExtractorValueMissing } result := make([]string, 0) for i, value := range values { if prefixLen == 0 { result = append(result, value) if i >= extractorLimit-1 { break } continue } if len(value) > prefixLen && strings.EqualFold(value[:prefixLen], valuePrefix) { result = append(result, value[prefixLen:]) if i >= extractorLimit-1 { break } } } if len(result) == 0 { if prefixLen > 0 { return nil, errHeaderExtractorValueInvalid } return nil, errHeaderExtractorValueMissing } return result, nil } } // valuesFromQuery returns a function that extracts values from the query string. func valuesFromQuery(param string) ValuesExtractor { return func(c echo.Context) ([]string, error) { result := c.QueryParams()[param] if len(result) == 0 { return nil, errQueryExtractorValueMissing } else if len(result) > extractorLimit-1 { result = result[:extractorLimit] } return result, nil } } // valuesFromParam returns a function that extracts values from the url param string. func valuesFromParam(param string) ValuesExtractor { return func(c echo.Context) ([]string, error) { result := make([]string, 0) paramVales := c.ParamValues() for i, p := range c.ParamNames() { if param == p { result = append(result, paramVales[i]) if i >= extractorLimit-1 { break } } } if len(result) == 0 { return nil, errParamExtractorValueMissing } return result, nil } } // valuesFromCookie returns a function that extracts values from the named cookie. func valuesFromCookie(name string) ValuesExtractor { return func(c echo.Context) ([]string, error) { cookies := c.Cookies() if len(cookies) == 0 { return nil, errCookieExtractorValueMissing } result := make([]string, 0) for i, cookie := range cookies { if name == cookie.Name { result = append(result, cookie.Value) if i >= extractorLimit-1 { break } } } if len(result) == 0 { return nil, errCookieExtractorValueMissing } return result, nil } } // valuesFromForm returns a function that extracts values from the form field. func valuesFromForm(name string) ValuesExtractor { return func(c echo.Context) ([]string, error) { if c.Request().Form == nil { _ = c.Request().ParseMultipartForm(32 << 20) // same what `c.Request().FormValue(name)` does } values := c.Request().Form[name] if len(values) == 0 { return nil, errFormExtractorValueMissing } if len(values) > extractorLimit-1 { values = values[:extractorLimit] } result := append([]string{}, values...) return result, nil } } golang-github-labstack-echo-4.11.1/middleware/extractor_test.go000066400000000000000000000373321445647333300245650ustar00rootroot00000000000000package middleware import ( "bytes" "fmt" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) type pathParam struct { name string value string } func setPathParams(c echo.Context, params []pathParam) { names := make([]string, 0, len(params)) values := make([]string, 0, len(params)) for _, pp := range params { names = append(names, pp.name) values = append(values, pp.value) } c.SetParamNames(names...) c.SetParamValues(values...) } func TestCreateExtractors(t *testing.T) { var testCases = []struct { name string givenRequest func() *http.Request givenPathParams []pathParam whenLoopups string expectValues []string expectCreateError string expectError string }{ { name: "ok, header", givenRequest: func() *http.Request { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, "Bearer token") return req }, whenLoopups: "header:Authorization:Bearer ", expectValues: []string{"token"}, }, { name: "ok, form", givenRequest: func() *http.Request { f := make(url.Values) f.Set("name", "Jon Snow") req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) return req }, whenLoopups: "form:name", expectValues: []string{"Jon Snow"}, }, { name: "ok, cookie", givenRequest: func() *http.Request { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderCookie, "_csrf=token") return req }, whenLoopups: "cookie:_csrf", expectValues: []string{"token"}, }, { name: "ok, param", givenPathParams: []pathParam{ {name: "id", value: "123"}, }, whenLoopups: "param:id", expectValues: []string{"123"}, }, { name: "ok, query", givenRequest: func() *http.Request { req := httptest.NewRequest(http.MethodGet, "/?id=999", nil) return req }, whenLoopups: "query:id", expectValues: []string{"999"}, }, { name: "nok, invalid lookup", whenLoopups: "query", expectCreateError: "extractor source for lookup could not be split into needed parts: query", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenRequest != nil { req = tc.givenRequest() } rec := httptest.NewRecorder() c := e.NewContext(req, rec) if tc.givenPathParams != nil { setPathParams(c, tc.givenPathParams) } extractors, err := CreateExtractors(tc.whenLoopups) if tc.expectCreateError != "" { assert.EqualError(t, err, tc.expectCreateError) return } assert.NoError(t, err) for _, e := range extractors { values, eErr := e(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, eErr, tc.expectError) return } assert.NoError(t, eErr) } }) } } func TestValuesFromHeader(t *testing.T) { exampleRequest := func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "basic dXNlcjpwYXNzd29yZA==") } var testCases = []struct { name string givenRequest func(req *http.Request) whenName string whenValuePrefix string expectValues []string expectError string }{ { name: "ok, single value", givenRequest: exampleRequest, whenName: echo.HeaderAuthorization, whenValuePrefix: "basic ", expectValues: []string{"dXNlcjpwYXNzd29yZA=="}, }, { name: "ok, single value, case insensitive", givenRequest: exampleRequest, whenName: echo.HeaderAuthorization, whenValuePrefix: "Basic ", expectValues: []string{"dXNlcjpwYXNzd29yZA=="}, }, { name: "ok, multiple value", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "basic dXNlcjpwYXNzd29yZA==") req.Header.Add(echo.HeaderAuthorization, "basic dGVzdDp0ZXN0") }, whenName: echo.HeaderAuthorization, whenValuePrefix: "basic ", expectValues: []string{"dXNlcjpwYXNzd29yZA==", "dGVzdDp0ZXN0"}, }, { name: "ok, empty prefix", givenRequest: exampleRequest, whenName: echo.HeaderAuthorization, whenValuePrefix: "", expectValues: []string{"basic dXNlcjpwYXNzd29yZA=="}, }, { name: "nok, no matching due different prefix", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "basic dXNlcjpwYXNzd29yZA==") req.Header.Add(echo.HeaderAuthorization, "basic dGVzdDp0ZXN0") }, whenName: echo.HeaderAuthorization, whenValuePrefix: "Bearer ", expectError: errHeaderExtractorValueInvalid.Error(), }, { name: "nok, no matching due different prefix", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "basic dXNlcjpwYXNzd29yZA==") req.Header.Add(echo.HeaderAuthorization, "basic dGVzdDp0ZXN0") }, whenName: echo.HeaderWWWAuthenticate, whenValuePrefix: "", expectError: errHeaderExtractorValueMissing.Error(), }, { name: "nok, no headers", givenRequest: nil, whenName: echo.HeaderAuthorization, whenValuePrefix: "basic ", expectError: errHeaderExtractorValueMissing.Error(), }, { name: "ok, prefix, cut values over extractorLimit", givenRequest: func(req *http.Request) { for i := 1; i <= 25; i++ { req.Header.Add(echo.HeaderAuthorization, fmt.Sprintf("basic %v", i)) } }, whenName: echo.HeaderAuthorization, whenValuePrefix: "basic ", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, { name: "ok, cut values over extractorLimit", givenRequest: func(req *http.Request) { for i := 1; i <= 25; i++ { req.Header.Add(echo.HeaderAuthorization, fmt.Sprintf("%v", i)) } }, whenName: echo.HeaderAuthorization, whenValuePrefix: "", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenRequest != nil { tc.givenRequest(req) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) extractor := valuesFromHeader(tc.whenName, tc.whenValuePrefix) values, err := extractor(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValuesFromQuery(t *testing.T) { var testCases = []struct { name string givenQueryPart string whenName string expectValues []string expectError string }{ { name: "ok, single value", givenQueryPart: "?id=123&name=test", whenName: "id", expectValues: []string{"123"}, }, { name: "ok, multiple value", givenQueryPart: "?id=123&id=456&name=test", whenName: "id", expectValues: []string{"123", "456"}, }, { name: "nok, missing value", givenQueryPart: "?id=123&name=test", whenName: "nope", expectError: errQueryExtractorValueMissing.Error(), }, { name: "ok, cut values over extractorLimit", givenQueryPart: "?name=test" + "&id=1&id=2&id=3&id=4&id=5&id=6&id=7&id=8&id=9&id=10" + "&id=11&id=12&id=13&id=14&id=15&id=16&id=17&id=18&id=19&id=20" + "&id=21&id=22&id=23&id=24&id=25", whenName: "id", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/"+tc.givenQueryPart, nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) extractor := valuesFromQuery(tc.whenName) values, err := extractor(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValuesFromParam(t *testing.T) { examplePathParams := []pathParam{ {name: "id", value: "123"}, {name: "gid", value: "456"}, {name: "gid", value: "789"}, } examplePathParams20 := make([]pathParam, 0) for i := 1; i < 25; i++ { examplePathParams20 = append(examplePathParams20, pathParam{name: "id", value: fmt.Sprintf("%v", i)}) } var testCases = []struct { name string givenPathParams []pathParam whenName string expectValues []string expectError string }{ { name: "ok, single value", givenPathParams: examplePathParams, whenName: "id", expectValues: []string{"123"}, }, { name: "ok, multiple value", givenPathParams: examplePathParams, whenName: "gid", expectValues: []string{"456", "789"}, }, { name: "nok, no values", givenPathParams: nil, whenName: "nope", expectValues: nil, expectError: errParamExtractorValueMissing.Error(), }, { name: "nok, no matching value", givenPathParams: examplePathParams, whenName: "nope", expectValues: nil, expectError: errParamExtractorValueMissing.Error(), }, { name: "ok, cut values over extractorLimit", givenPathParams: examplePathParams20, whenName: "id", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) if tc.givenPathParams != nil { setPathParams(c, tc.givenPathParams) } extractor := valuesFromParam(tc.whenName) values, err := extractor(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValuesFromCookie(t *testing.T) { exampleRequest := func(req *http.Request) { req.Header.Set(echo.HeaderCookie, "_csrf=token") } var testCases = []struct { name string givenRequest func(req *http.Request) whenName string expectValues []string expectError string }{ { name: "ok, single value", givenRequest: exampleRequest, whenName: "_csrf", expectValues: []string{"token"}, }, { name: "ok, multiple value", givenRequest: func(req *http.Request) { req.Header.Add(echo.HeaderCookie, "_csrf=token") req.Header.Add(echo.HeaderCookie, "_csrf=token2") }, whenName: "_csrf", expectValues: []string{"token", "token2"}, }, { name: "nok, no matching cookie", givenRequest: exampleRequest, whenName: "xxx", expectValues: nil, expectError: errCookieExtractorValueMissing.Error(), }, { name: "nok, no cookies at all", givenRequest: nil, whenName: "xxx", expectValues: nil, expectError: errCookieExtractorValueMissing.Error(), }, { name: "ok, cut values over extractorLimit", givenRequest: func(req *http.Request) { for i := 1; i < 25; i++ { req.Header.Add(echo.HeaderCookie, fmt.Sprintf("_csrf=%v", i)) } }, whenName: "_csrf", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenRequest != nil { tc.givenRequest(req) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) extractor := valuesFromCookie(tc.whenName) values, err := extractor(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestValuesFromForm(t *testing.T) { examplePostFormRequest := func(mod func(v *url.Values)) *http.Request { f := make(url.Values) f.Set("name", "Jon Snow") f.Set("emails[]", "jon@labstack.com") if mod != nil { mod(&f) } req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) return req } exampleGetFormRequest := func(mod func(v *url.Values)) *http.Request { f := make(url.Values) f.Set("name", "Jon Snow") f.Set("emails[]", "jon@labstack.com") if mod != nil { mod(&f) } req := httptest.NewRequest(http.MethodGet, "/?"+f.Encode(), nil) return req } exampleMultiPartFormRequest := func(mod func(w *multipart.Writer)) *http.Request { var b bytes.Buffer w := multipart.NewWriter(&b) w.WriteField("name", "Jon Snow") w.WriteField("emails[]", "jon@labstack.com") if mod != nil { mod(w) } fw, _ := w.CreateFormFile("upload", "my.file") fw.Write([]byte(`
hi
`)) w.Close() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(b.String())) req.Header.Add(echo.HeaderContentType, w.FormDataContentType()) return req } var testCases = []struct { name string givenRequest *http.Request whenName string expectValues []string expectError string }{ { name: "ok, POST form, single value", givenRequest: examplePostFormRequest(nil), whenName: "emails[]", expectValues: []string{"jon@labstack.com"}, }, { name: "ok, POST form, multiple value", givenRequest: examplePostFormRequest(func(v *url.Values) { v.Add("emails[]", "snow@labstack.com") }), whenName: "emails[]", expectValues: []string{"jon@labstack.com", "snow@labstack.com"}, }, { name: "ok, POST multipart/form, multiple value", givenRequest: exampleMultiPartFormRequest(func(w *multipart.Writer) { w.WriteField("emails[]", "snow@labstack.com") }), whenName: "emails[]", expectValues: []string{"jon@labstack.com", "snow@labstack.com"}, }, { name: "ok, GET form, single value", givenRequest: exampleGetFormRequest(nil), whenName: "emails[]", expectValues: []string{"jon@labstack.com"}, }, { name: "ok, GET form, multiple value", givenRequest: examplePostFormRequest(func(v *url.Values) { v.Add("emails[]", "snow@labstack.com") }), whenName: "emails[]", expectValues: []string{"jon@labstack.com", "snow@labstack.com"}, }, { name: "nok, POST form, value missing", givenRequest: examplePostFormRequest(nil), whenName: "nope", expectError: errFormExtractorValueMissing.Error(), }, { name: "ok, cut values over extractorLimit", givenRequest: examplePostFormRequest(func(v *url.Values) { for i := 1; i < 25; i++ { v.Add("id[]", fmt.Sprintf("%v", i)) } }), whenName: "id[]", expectValues: []string{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() req := tc.givenRequest rec := httptest.NewRecorder() c := e.NewContext(req, rec) extractor := valuesFromForm(tc.whenName) values, err := extractor(c) assert.Equal(t, tc.expectValues, values) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } golang-github-labstack-echo-4.11.1/middleware/jwt.go000066400000000000000000000246541445647333300223220ustar00rootroot00000000000000//go:build go1.15 // +build go1.15 package middleware import ( "errors" "fmt" "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "net/http" "reflect" ) type ( // JWTConfig defines the config for JWT middleware. JWTConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // BeforeFunc defines a function which is executed just before the middleware. BeforeFunc BeforeFunc // SuccessHandler defines a function which is executed for a valid token before middleware chain continues with next // middleware or handler. SuccessHandler JWTSuccessHandler // ErrorHandler defines a function which is executed for an invalid token. // It may be used to define a custom JWT error. ErrorHandler JWTErrorHandler // ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context. ErrorHandlerWithContext JWTErrorHandlerWithContext // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandlerWithContext decides to // ignore the error (by returning `nil`). // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. // In that case you can use ErrorHandlerWithContext to set a default public JWT token value in the request context // and continue. Some logic down the remaining execution chain needs to check that (public) token value then. ContinueOnIgnoredError bool // Signing key to validate token. // This is one of the three options to provide a token validation key. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // Required if neither user-defined KeyFunc nor SigningKeys is provided. SigningKey interface{} // Map of signing keys to validate token with kid field usage. // This is one of the three options to provide a token validation key. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // Required if neither user-defined KeyFunc nor SigningKey is provided. SigningKeys map[string]interface{} // Signing method used to check the token's signing algorithm. // Optional. Default value HS256. SigningMethod string // Context key to store user information from the token into context. // Optional. Default value "user". ContextKey string // Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation. // Not used if custom ParseTokenFunc is set. // Optional. Default value jwt.MapClaims Claims jwt.Claims // TokenLookup is a string in the form of ":" or ":,:" that is used // to extract token from the request. // Optional. Default value "header:Authorization". // Possible values: // - "header:" or "header::" // `` is argument value to cut/trim prefix of the extracted value. This is useful if header // value has static prefix like `Authorization: ` where part that we // want to cut is ` ` note the space at the end. // In case of JWT tokens `Authorization: Bearer ` prefix we cut is `Bearer `. // If prefix is left empty the whole value is returned. // - "query:" // - "param:" // - "cookie:" // - "form:" // Multiple sources example: // - "header:Authorization,cookie:myowncookie" TokenLookup string // TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context. // This is one of the two options to provide a token extractor. // The order of precedence is user-defined TokenLookupFuncs, and TokenLookup. // You can also provide both if you want. TokenLookupFuncs []ValuesExtractor // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". AuthScheme string // KeyFunc defines a user-defined function that supplies the public key for a token validation. // The function shall take care of verifying the signing algorithm and selecting the proper key. // A user-defined KeyFunc can be useful if tokens are issued by an external party. // Used by default ParseTokenFunc implementation. // // When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. // This is one of the three options to provide a token validation key. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // Required if neither SigningKeys nor SigningKey is provided. // Not used if custom ParseTokenFunc is set. // Default to an internal implementation verifying the signing algorithm and selecting the proper key. KeyFunc jwt.Keyfunc // ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token // parsing fails or parsed token is invalid. // Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library ParseTokenFunc func(auth string, c echo.Context) (interface{}, error) } // JWTSuccessHandler defines a function which is executed for a valid token. JWTSuccessHandler func(c echo.Context) // JWTErrorHandler defines a function which is executed for an invalid token. JWTErrorHandler func(err error) error // JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context. JWTErrorHandlerWithContext func(err error, c echo.Context) error ) // Algorithms const ( AlgorithmHS256 = "HS256" ) // Errors var ( ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt") ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt") ) var ( // DefaultJWTConfig is the default JWT auth middleware config. DefaultJWTConfig = JWTConfig{ Skipper: DefaultSkipper, SigningMethod: AlgorithmHS256, ContextKey: "user", TokenLookup: "header:" + echo.HeaderAuthorization, TokenLookupFuncs: nil, AuthScheme: "Bearer", Claims: jwt.MapClaims{}, KeyFunc: nil, } ) // JWT returns a JSON Web Token (JWT) auth middleware. // // For valid token, it sets the user in context and calls next handler. // For invalid token, it returns "401 - Unauthorized" error. // For missing token, it returns "400 - Bad Request" error. // // See: https://jwt.io/introduction // See `JWTConfig.TokenLookup` // // Deprecated: Please use https://github.com/labstack/echo-jwt instead func JWT(key interface{}) echo.MiddlewareFunc { c := DefaultJWTConfig c.SigningKey = key return JWTWithConfig(c) } // JWTWithConfig returns a JWT auth middleware with config. // See: `JWT()`. // // Deprecated: Please use https://github.com/labstack/echo-jwt instead func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultJWTConfig.Skipper } if config.SigningKey == nil && len(config.SigningKeys) == 0 && config.KeyFunc == nil && config.ParseTokenFunc == nil { panic("echo: jwt middleware requires signing key") } if config.SigningMethod == "" { config.SigningMethod = DefaultJWTConfig.SigningMethod } if config.ContextKey == "" { config.ContextKey = DefaultJWTConfig.ContextKey } if config.Claims == nil { config.Claims = DefaultJWTConfig.Claims } if config.TokenLookup == "" && len(config.TokenLookupFuncs) == 0 { config.TokenLookup = DefaultJWTConfig.TokenLookup } if config.AuthScheme == "" { config.AuthScheme = DefaultJWTConfig.AuthScheme } if config.KeyFunc == nil { config.KeyFunc = config.defaultKeyFunc } if config.ParseTokenFunc == nil { config.ParseTokenFunc = config.defaultParseToken } extractors, cErr := createExtractors(config.TokenLookup, config.AuthScheme) if cErr != nil { panic(cErr) } if len(config.TokenLookupFuncs) > 0 { extractors = append(config.TokenLookupFuncs, extractors...) } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } if config.BeforeFunc != nil { config.BeforeFunc(c) } var lastExtractorErr error var lastTokenErr error for _, extractor := range extractors { auths, err := extractor(c) if err != nil { lastExtractorErr = ErrJWTMissing // backwards compatibility: all extraction errors are same (unlike KeyAuth) continue } for _, auth := range auths { token, err := config.ParseTokenFunc(auth, c) if err != nil { lastTokenErr = err continue } // Store user information from token into context. c.Set(config.ContextKey, token) if config.SuccessHandler != nil { config.SuccessHandler(c) } return next(c) } } // we are here only when we did not successfully extract or parse any of the tokens err := lastTokenErr if err == nil { // prioritize token errors over extracting errors err = lastExtractorErr } if config.ErrorHandler != nil { return config.ErrorHandler(err) } if config.ErrorHandlerWithContext != nil { tmpErr := config.ErrorHandlerWithContext(err, c) if config.ContinueOnIgnoredError && tmpErr == nil { return next(c) } return tmpErr } // backwards compatible errors codes if lastTokenErr != nil { return &echo.HTTPError{ Code: ErrJWTInvalid.Code, Message: ErrJWTInvalid.Message, Internal: err, } } return err // this is lastExtractorErr value } } } func (config *JWTConfig) defaultParseToken(auth string, c echo.Context) (interface{}, error) { var token *jwt.Token var err error // Issue #647, #656 if _, ok := config.Claims.(jwt.MapClaims); ok { token, err = jwt.Parse(auth, config.KeyFunc) } else { t := reflect.ValueOf(config.Claims).Type().Elem() claims := reflect.New(t).Interface().(jwt.Claims) token, err = jwt.ParseWithClaims(auth, claims, config.KeyFunc) } if err != nil { return nil, err } if !token.Valid { return nil, errors.New("invalid token") } return token, nil } // defaultKeyFunc returns a signing key of the given token. func (config *JWTConfig) defaultKeyFunc(t *jwt.Token) (interface{}, error) { // Check the signing method if t.Method.Alg() != config.SigningMethod { return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"]) } if len(config.SigningKeys) > 0 { if kid, ok := t.Header["kid"].(string); ok { if key, ok := config.SigningKeys[kid]; ok { return key, nil } } return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"]) } return config.SigningKey, nil } golang-github-labstack-echo-4.11.1/middleware/jwt_test.go000066400000000000000000000537731445647333300233650ustar00rootroot00000000000000//go:build go1.15 // +build go1.15 package middleware import ( "errors" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) // jwtCustomInfo defines some custom types we're going to use within our tokens. type jwtCustomInfo struct { Name string `json:"name"` Admin bool `json:"admin"` } // jwtCustomClaims are custom claims expanding default ones. type jwtCustomClaims struct { *jwt.StandardClaims jwtCustomInfo } func TestJWT(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { token := c.Get("user").(*jwt.Token) return c.JSON(http.StatusOK, token.Claims) }) e.Use(JWT([]byte("secret"))) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) assert.Equal(t, `{"admin":true,"name":"John Doe","sub":"1234567890"}`+"\n", res.Body.String()) } func TestJWTRace(t *testing.T) { e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } initialToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" raceToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlJhY2UgQ29uZGl0aW9uIiwiYWRtaW4iOmZhbHNlfQ.Xzkx9mcgGqYMTkuxSCbJ67lsDyk5J2aB7hu65cEE-Ss" validKey := []byte("secret") h := JWTWithConfig(JWTConfig{ Claims: &jwtCustomClaims{}, SigningKey: validKey, })(handler) makeReq := func(token string) echo.Context { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" "+token) c := e.NewContext(req, res) assert.NoError(t, h(c)) return c } c := makeReq(initialToken) user := c.Get("user").(*jwt.Token) claims := user.Claims.(*jwtCustomClaims) assert.Equal(t, claims.Name, "John Doe") makeReq(raceToken) user = c.Get("user").(*jwt.Token) claims = user.Claims.(*jwtCustomClaims) // Initial context should still be "John Doe", not "Race Condition" assert.Equal(t, claims.Name, "John Doe") assert.Equal(t, claims.Admin, true) } func TestJWTConfig(t *testing.T) { handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" validKey := []byte("secret") invalidKey := []byte("invalid-key") validAuth := DefaultJWTConfig.AuthScheme + " " + token testCases := []struct { name string expPanic bool expErrCode int // 0 for Success config JWTConfig reqURL string // "/" if empty hdrAuth string hdrCookie string // test.Request doesn't provide SetCookie(); use name=val formValues map[string]string }{ { name: "No signing key provided", expPanic: true, }, { name: "Unexpected signing method", expErrCode: http.StatusBadRequest, config: JWTConfig{ SigningKey: validKey, SigningMethod: "RS256", }, }, { name: "Invalid key", expErrCode: http.StatusUnauthorized, hdrAuth: validAuth, config: JWTConfig{SigningKey: invalidKey}, }, { name: "Valid JWT", hdrAuth: validAuth, config: JWTConfig{SigningKey: validKey}, }, { name: "Valid JWT with custom AuthScheme", hdrAuth: "Token" + " " + token, config: JWTConfig{AuthScheme: "Token", SigningKey: validKey}, }, { name: "Valid JWT with custom claims", hdrAuth: validAuth, config: JWTConfig{ Claims: &jwtCustomClaims{}, SigningKey: []byte("secret"), }, }, { name: "Invalid Authorization header", hdrAuth: "invalid-auth", expErrCode: http.StatusBadRequest, config: JWTConfig{SigningKey: validKey}, }, { name: "Empty header auth field", config: JWTConfig{SigningKey: validKey}, expErrCode: http.StatusBadRequest, }, { name: "Valid query method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwt=" + token, }, { name: "Invalid query param name", config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwtxyz=" + token, expErrCode: http.StatusBadRequest, }, { name: "Invalid query param value", config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwt=invalid-token", expErrCode: http.StatusUnauthorized, }, { name: "Empty query", config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b", expErrCode: http.StatusBadRequest, }, { name: "Valid param method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "param:jwt", }, reqURL: "/" + token, }, { name: "Valid cookie method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, hdrCookie: "jwt=" + token, }, { name: "Multiple jwt lookuop", config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt,cookie:jwt", }, hdrCookie: "jwt=" + token, }, { name: "Invalid token with cookie method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, expErrCode: http.StatusUnauthorized, hdrCookie: "jwt=invalid", }, { name: "Empty cookie", config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, expErrCode: http.StatusBadRequest, }, { name: "Valid form method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, formValues: map[string]string{"jwt": token}, }, { name: "Invalid token with form method", config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, expErrCode: http.StatusUnauthorized, formValues: map[string]string{"jwt": "invalid"}, }, { name: "Empty form field", config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, expErrCode: http.StatusBadRequest, }, { name: "Valid JWT with a valid key using a user-defined KeyFunc", hdrAuth: validAuth, config: JWTConfig{ KeyFunc: func(*jwt.Token) (interface{}, error) { return validKey, nil }, }, }, { name: "Valid JWT with an invalid key using a user-defined KeyFunc", hdrAuth: validAuth, config: JWTConfig{ KeyFunc: func(*jwt.Token) (interface{}, error) { return invalidKey, nil }, }, expErrCode: http.StatusUnauthorized, }, { name: "Token verification does not pass using a user-defined KeyFunc", hdrAuth: validAuth, config: JWTConfig{ KeyFunc: func(*jwt.Token) (interface{}, error) { return nil, errors.New("faulty KeyFunc") }, }, expErrCode: http.StatusUnauthorized, }, { name: "Valid JWT with lower case AuthScheme", hdrAuth: strings.ToLower(DefaultJWTConfig.AuthScheme) + " " + token, config: JWTConfig{SigningKey: validKey}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() if tc.reqURL == "" { tc.reqURL = "/" } var req *http.Request if len(tc.formValues) > 0 { form := url.Values{} for k, v := range tc.formValues { form.Set(k, v) } req = httptest.NewRequest(http.MethodPost, tc.reqURL, strings.NewReader(form.Encode())) req.Header.Set(echo.HeaderContentType, "application/x-www-form-urlencoded") req.ParseForm() } else { req = httptest.NewRequest(http.MethodGet, tc.reqURL, nil) } res := httptest.NewRecorder() req.Header.Set(echo.HeaderAuthorization, tc.hdrAuth) req.Header.Set(echo.HeaderCookie, tc.hdrCookie) c := e.NewContext(req, res) if tc.reqURL == "/"+token { c.SetParamNames("jwt") c.SetParamValues(token) } if tc.expPanic { assert.Panics(t, func() { JWTWithConfig(tc.config) }, tc.name) return } if tc.expErrCode != 0 { h := JWTWithConfig(tc.config)(handler) he := h(c).(*echo.HTTPError) assert.Equal(t, tc.expErrCode, he.Code, tc.name) return } h := JWTWithConfig(tc.config)(handler) if assert.NoError(t, h(c), tc.name) { user := c.Get("user").(*jwt.Token) switch claims := user.Claims.(type) { case jwt.MapClaims: assert.Equal(t, claims["name"], "John Doe", tc.name) case *jwtCustomClaims: assert.Equal(t, claims.Name, "John Doe", tc.name) assert.Equal(t, claims.Admin, true, tc.name) default: panic("unexpected type of claims") } } }) } } func TestJWTwithKID(t *testing.T) { e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } firstToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImZpcnN0T25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.w5VGpHOe0jlNgf7jMVLHzIYH_XULmpUlreJnilwSkWk" secondToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InNlY29uZE9uZSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.sdghDYQ85jdh0hgQ6bKbMguLI_NSPYWjkhVJkee-yZM" wrongToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InNlY29uZE9uZSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.RyhLybtVLpoewF6nz9YN79oXo32kAtgUxp8FNwTkb90" staticToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.1_-XFYUPpJfgsaGwYhgZEt7hfySMg-a3GN-nfZmbW7o" validKeys := map[string]interface{}{"firstOne": []byte("first_secret"), "secondOne": []byte("second_secret")} invalidKeys := map[string]interface{}{"thirdOne": []byte("third_secret")} staticSecret := []byte("static_secret") invalidStaticSecret := []byte("invalid_secret") for _, tc := range []struct { expErrCode int // 0 for Success config JWTConfig hdrAuth string info string }{ { hdrAuth: DefaultJWTConfig.AuthScheme + " " + firstToken, config: JWTConfig{SigningKeys: validKeys}, info: "First token valid", }, { hdrAuth: DefaultJWTConfig.AuthScheme + " " + secondToken, config: JWTConfig{SigningKeys: validKeys}, info: "Second token valid", }, { expErrCode: http.StatusUnauthorized, hdrAuth: DefaultJWTConfig.AuthScheme + " " + wrongToken, config: JWTConfig{SigningKeys: validKeys}, info: "Wrong key id token", }, { hdrAuth: DefaultJWTConfig.AuthScheme + " " + staticToken, config: JWTConfig{SigningKey: staticSecret}, info: "Valid static secret token", }, { expErrCode: http.StatusUnauthorized, hdrAuth: DefaultJWTConfig.AuthScheme + " " + staticToken, config: JWTConfig{SigningKey: invalidStaticSecret}, info: "Invalid static secret", }, { expErrCode: http.StatusUnauthorized, hdrAuth: DefaultJWTConfig.AuthScheme + " " + firstToken, config: JWTConfig{SigningKeys: invalidKeys}, info: "Invalid keys first token", }, { expErrCode: http.StatusUnauthorized, hdrAuth: DefaultJWTConfig.AuthScheme + " " + secondToken, config: JWTConfig{SigningKeys: invalidKeys}, info: "Invalid keys second token", }, } { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() req.Header.Set(echo.HeaderAuthorization, tc.hdrAuth) c := e.NewContext(req, res) if tc.expErrCode != 0 { h := JWTWithConfig(tc.config)(handler) he := h(c).(*echo.HTTPError) assert.Equal(t, tc.expErrCode, he.Code, tc.info) continue } h := JWTWithConfig(tc.config)(handler) if assert.NoError(t, h(c), tc.info) { user := c.Get("user").(*jwt.Token) switch claims := user.Claims.(type) { case jwt.MapClaims: assert.Equal(t, claims["name"], "John Doe", tc.info) case *jwtCustomClaims: assert.Equal(t, claims.Name, "John Doe", tc.info) assert.Equal(t, claims.Admin, true, tc.info) default: panic("unexpected type of claims") } } } } func TestJWTConfig_skipper(t *testing.T) { e := echo.New() e.Use(JWTWithConfig(JWTConfig{ Skipper: func(context echo.Context) bool { return true // skip everything }, SigningKey: []byte("secret"), })) isCalled := false e.GET("/", func(c echo.Context) error { isCalled = true return c.String(http.StatusTeapot, "test") }) req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusTeapot, res.Code) assert.True(t, isCalled) } func TestJWTConfig_BeforeFunc(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusTeapot, "test") }) isCalled := false e.Use(JWTWithConfig(JWTConfig{ BeforeFunc: func(context echo.Context) { isCalled = true }, SigningKey: []byte("secret"), })) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusTeapot, res.Code) assert.True(t, isCalled) } func TestJWTConfig_extractorErrorHandling(t *testing.T) { var testCases = []struct { name string given JWTConfig expectStatusCode int }{ { name: "ok, ErrorHandler is executed", given: JWTConfig{ SigningKey: []byte("secret"), ErrorHandler: func(err error) error { return echo.NewHTTPError(http.StatusTeapot, "custom_error") }, }, expectStatusCode: http.StatusTeapot, }, { name: "ok, ErrorHandlerWithContext is executed", given: JWTConfig{ SigningKey: []byte("secret"), ErrorHandlerWithContext: func(err error, context echo.Context) error { return echo.NewHTTPError(http.StatusTeapot, "custom_error") }, }, expectStatusCode: http.StatusTeapot, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusNotImplemented, "should not end up here") }) e.Use(JWTWithConfig(tc.given)) req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, tc.expectStatusCode, res.Code) }) } } func TestJWTConfig_parseTokenErrorHandling(t *testing.T) { var testCases = []struct { name string given JWTConfig expectErr string }{ { name: "ok, ErrorHandler is executed", given: JWTConfig{ SigningKey: []byte("secret"), ErrorHandler: func(err error) error { return echo.NewHTTPError(http.StatusTeapot, "ErrorHandler: "+err.Error()) }, }, expectErr: "{\"message\":\"ErrorHandler: parsing failed\"}\n", }, { name: "ok, ErrorHandlerWithContext is executed", given: JWTConfig{ SigningKey: []byte("secret"), ErrorHandlerWithContext: func(err error, context echo.Context) error { return echo.NewHTTPError(http.StatusTeapot, "ErrorHandlerWithContext: "+err.Error()) }, }, expectErr: "{\"message\":\"ErrorHandlerWithContext: parsing failed\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() //e.Debug = true e.GET("/", func(c echo.Context) error { return c.String(http.StatusNotImplemented, "should not end up here") }) config := tc.given parseTokenCalled := false config.ParseTokenFunc = func(auth string, c echo.Context) (interface{}, error) { parseTokenCalled = true return nil, errors.New("parsing failed") } e.Use(JWTWithConfig(config)) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusTeapot, res.Code) assert.Equal(t, tc.expectErr, res.Body.String()) assert.True(t, parseTokenCalled) }) } } func TestJWTConfig_custom_ParseTokenFunc_Keyfunc(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusTeapot, "test") }) // example of minimal custom ParseTokenFunc implementation. Allows you to use different versions of `github.com/golang-jwt/jwt` // with current JWT middleware signingKey := []byte("secret") config := JWTConfig{ ParseTokenFunc: func(auth string, c echo.Context) (interface{}, error) { keyFunc := func(t *jwt.Token) (interface{}, error) { if t.Method.Alg() != "HS256" { return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"]) } return signingKey, nil } // claims are of type `jwt.MapClaims` when token is created with `jwt.Parse` token, err := jwt.Parse(auth, keyFunc) if err != nil { return nil, err } if !token.Valid { return nil, errors.New("invalid token") } return token, nil }, } e.Use(JWTWithConfig(config)) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusTeapot, res.Code) } func TestJWTConfig_TokenLookupFuncs(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { token := c.Get("user").(*jwt.Token) return c.JSON(http.StatusOK, token.Claims) }) e.Use(JWTWithConfig(JWTConfig{ TokenLookupFuncs: []ValuesExtractor{ func(c echo.Context) ([]string, error) { return []string{c.Request().Header.Get("X-API-Key")}, nil }, }, SigningKey: []byte("secret"), })) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-API-Key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) assert.Equal(t, `{"admin":true,"name":"John Doe","sub":"1234567890"}`+"\n", res.Body.String()) } func TestJWTConfig_SuccessHandler(t *testing.T) { var testCases = []struct { name string givenToken string expectCalled bool expectStatus int }{ { name: "ok, success handler is called", givenToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ", expectCalled: true, expectStatus: http.StatusOK, }, { name: "nok, success handler is not called", givenToken: "x.x.x", expectCalled: false, expectStatus: http.StatusUnauthorized, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { token := c.Get("user").(*jwt.Token) return c.JSON(http.StatusOK, token.Claims) }) wasCalled := false e.Use(JWTWithConfig(JWTConfig{ SuccessHandler: func(c echo.Context) { wasCalled = true }, SigningKey: []byte("secret"), })) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, "bearer "+tc.givenToken) res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, tc.expectCalled, wasCalled) assert.Equal(t, tc.expectStatus, res.Code) }) } } func TestJWTConfig_ContinueOnIgnoredError(t *testing.T) { var testCases = []struct { name string whenContinueOnIgnoredError bool givenToken string expectStatus int expectBody string }{ { name: "no error handler is called", whenContinueOnIgnoredError: true, givenToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ", expectStatus: http.StatusTeapot, expectBody: "", }, { name: "ContinueOnIgnoredError is false and error handler is called for missing token", whenContinueOnIgnoredError: false, givenToken: "", // empty response with 200. This emulates previous behaviour when error handler swallowed the error expectStatus: http.StatusOK, expectBody: "", }, { name: "error handler is called for missing token", whenContinueOnIgnoredError: true, givenToken: "", expectStatus: http.StatusTeapot, expectBody: "public-token", }, { name: "error handler is called for invalid token", whenContinueOnIgnoredError: true, givenToken: "x.x.x", expectStatus: http.StatusUnauthorized, expectBody: "{\"message\":\"Unauthorized\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { testValue, _ := c.Get("test").(string) return c.String(http.StatusTeapot, testValue) }) e.Use(JWTWithConfig(JWTConfig{ ContinueOnIgnoredError: tc.whenContinueOnIgnoredError, SigningKey: []byte("secret"), ErrorHandlerWithContext: func(err error, c echo.Context) error { if err == ErrJWTMissing { c.Set("test", "public-token") return nil } return echo.ErrUnauthorized }, })) req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenToken != "" { req.Header.Set(echo.HeaderAuthorization, "bearer "+tc.givenToken) } res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, tc.expectStatus, res.Code) assert.Equal(t, tc.expectBody, res.Body.String()) }) } } golang-github-labstack-echo-4.11.1/middleware/key_auth.go000066400000000000000000000133141445647333300233160ustar00rootroot00000000000000package middleware import ( "errors" "github.com/labstack/echo/v4" "net/http" ) type ( // KeyAuthConfig defines the config for KeyAuth middleware. KeyAuthConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // KeyLookup is a string in the form of ":" or ":,:" that is used // to extract key from the request. // Optional. Default value "header:Authorization". // Possible values: // - "header:" or "header::" // `` is argument value to cut/trim prefix of the extracted value. This is useful if header // value has static prefix like `Authorization: ` where part that we // want to cut is ` ` note the space at the end. // In case of basic authentication `Authorization: Basic ` prefix we want to remove is `Basic `. // - "query:" // - "form:" // - "cookie:" // Multiple sources example: // - "header:Authorization,header:X-Api-Key" KeyLookup string // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". AuthScheme string // Validator is a function to validate key. // Required. Validator KeyAuthValidator // ErrorHandler defines a function which is executed for an invalid key. // It may be used to define a custom error. ErrorHandler KeyAuthErrorHandler // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to // ignore the error (by returning `nil`). // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. // In that case you can use ErrorHandler to set a default public key auth value in the request context // and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then. ContinueOnIgnoredError bool } // KeyAuthValidator defines a function to validate KeyAuth credentials. KeyAuthValidator func(auth string, c echo.Context) (bool, error) // KeyAuthErrorHandler defines a function which is executed for an invalid key. KeyAuthErrorHandler func(err error, c echo.Context) error ) var ( // DefaultKeyAuthConfig is the default KeyAuth middleware config. DefaultKeyAuthConfig = KeyAuthConfig{ Skipper: DefaultSkipper, KeyLookup: "header:" + echo.HeaderAuthorization, AuthScheme: "Bearer", } ) // ErrKeyAuthMissing is error type when KeyAuth middleware is unable to extract value from lookups type ErrKeyAuthMissing struct { Err error } // Error returns errors text func (e *ErrKeyAuthMissing) Error() string { return e.Err.Error() } // Unwrap unwraps error func (e *ErrKeyAuthMissing) Unwrap() error { return e.Err } // KeyAuth returns an KeyAuth middleware. // // For valid key it calls the next handler. // For invalid key, it sends "401 - Unauthorized" response. // For missing key, it sends "400 - Bad Request" response. func KeyAuth(fn KeyAuthValidator) echo.MiddlewareFunc { c := DefaultKeyAuthConfig c.Validator = fn return KeyAuthWithConfig(c) } // KeyAuthWithConfig returns an KeyAuth middleware with config. // See `KeyAuth()`. func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultKeyAuthConfig.Skipper } // Defaults if config.AuthScheme == "" { config.AuthScheme = DefaultKeyAuthConfig.AuthScheme } if config.KeyLookup == "" { config.KeyLookup = DefaultKeyAuthConfig.KeyLookup } if config.Validator == nil { panic("echo: key-auth middleware requires a validator function") } extractors, cErr := createExtractors(config.KeyLookup, config.AuthScheme) if cErr != nil { panic(cErr) } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } var lastExtractorErr error var lastValidatorErr error for _, extractor := range extractors { keys, err := extractor(c) if err != nil { lastExtractorErr = err continue } for _, key := range keys { valid, err := config.Validator(key, c) if err != nil { lastValidatorErr = err continue } if valid { return next(c) } lastValidatorErr = errors.New("invalid key") } } // we are here only when we did not successfully extract and validate any of keys err := lastValidatorErr if err == nil { // prioritize validator errors over extracting errors // ugly part to preserve backwards compatible errors. someone could rely on them if lastExtractorErr == errQueryExtractorValueMissing { err = errors.New("missing key in the query string") } else if lastExtractorErr == errCookieExtractorValueMissing { err = errors.New("missing key in cookies") } else if lastExtractorErr == errFormExtractorValueMissing { err = errors.New("missing key in the form") } else if lastExtractorErr == errHeaderExtractorValueMissing { err = errors.New("missing key in request header") } else if lastExtractorErr == errHeaderExtractorValueInvalid { err = errors.New("invalid key in the request header") } else { err = lastExtractorErr } err = &ErrKeyAuthMissing{Err: err} } if config.ErrorHandler != nil { tmpErr := config.ErrorHandler(err, c) if config.ContinueOnIgnoredError && tmpErr == nil { return next(c) } return tmpErr } if lastValidatorErr != nil { // prioritize validator errors over extracting errors return &echo.HTTPError{ Code: http.StatusUnauthorized, Message: "Unauthorized", Internal: lastValidatorErr, } } return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } } } golang-github-labstack-echo-4.11.1/middleware/key_auth_test.go000066400000000000000000000250131445647333300243540ustar00rootroot00000000000000package middleware import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func testKeyValidator(key string, c echo.Context) (bool, error) { switch key { case "valid-key": return true, nil case "error-key": return false, errors.New("some user defined error") default: return false, nil } } func TestKeyAuth(t *testing.T) { handlerCalled := false handler := func(c echo.Context) error { handlerCalled = true return c.String(http.StatusOK, "test") } middlewareChain := KeyAuth(testKeyValidator)(handler) e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderAuthorization, "Bearer valid-key") rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := middlewareChain(c) assert.NoError(t, err) assert.True(t, handlerCalled) } func TestKeyAuthWithConfig(t *testing.T) { var testCases = []struct { name string givenRequestFunc func() *http.Request givenRequest func(req *http.Request) whenConfig func(conf *KeyAuthConfig) expectHandlerCalled bool expectError string }{ { name: "ok, defaults, key from header", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bearer valid-key") }, expectHandlerCalled: true, }, { name: "ok, custom skipper", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bearer error-key") }, whenConfig: func(conf *KeyAuthConfig) { conf.Skipper = func(context echo.Context) bool { return true } }, expectHandlerCalled: true, }, { name: "nok, defaults, invalid key from header, Authorization: Bearer", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bearer invalid-key") }, expectHandlerCalled: false, expectError: "code=401, message=Unauthorized, internal=invalid key", }, { name: "nok, defaults, invalid scheme in header", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bear valid-key") }, expectHandlerCalled: false, expectError: "code=400, message=invalid key in the request header", }, { name: "nok, defaults, missing header", givenRequest: func(req *http.Request) {}, expectHandlerCalled: false, expectError: "code=400, message=missing key in request header", }, { name: "ok, custom key lookup from multiple places, query and header", givenRequest: func(req *http.Request) { req.URL.RawQuery = "key=invalid-key" req.Header.Set("API-Key", "valid-key") }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "query:key,header:API-Key" }, expectHandlerCalled: true, }, { name: "ok, custom key lookup, header", givenRequest: func(req *http.Request) { req.Header.Set("API-Key", "valid-key") }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "header:API-Key" }, expectHandlerCalled: true, }, { name: "nok, custom key lookup, missing header", givenRequest: func(req *http.Request) { }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "header:API-Key" }, expectHandlerCalled: false, expectError: "code=400, message=missing key in request header", }, { name: "ok, custom key lookup, query", givenRequest: func(req *http.Request) { q := req.URL.Query() q.Add("key", "valid-key") req.URL.RawQuery = q.Encode() }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "query:key" }, expectHandlerCalled: true, }, { name: "nok, custom key lookup, missing query param", whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "query:key" }, expectHandlerCalled: false, expectError: "code=400, message=missing key in the query string", }, { name: "ok, custom key lookup, form", givenRequestFunc: func() *http.Request { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("key=valid-key")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) return req }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "form:key" }, expectHandlerCalled: true, }, { name: "nok, custom key lookup, missing key in form", givenRequestFunc: func() *http.Request { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("xxx=valid-key")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) return req }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "form:key" }, expectHandlerCalled: false, expectError: "code=400, message=missing key in the form", }, { name: "ok, custom key lookup, cookie", givenRequest: func(req *http.Request) { req.AddCookie(&http.Cookie{ Name: "key", Value: "valid-key", }) q := req.URL.Query() q.Add("key", "valid-key") req.URL.RawQuery = q.Encode() }, whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "cookie:key" }, expectHandlerCalled: true, }, { name: "nok, custom key lookup, missing cookie param", whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "cookie:key" }, expectHandlerCalled: false, expectError: "code=400, message=missing key in cookies", }, { name: "nok, custom errorHandler, error from extractor", whenConfig: func(conf *KeyAuthConfig) { conf.KeyLookup = "header:token" conf.ErrorHandler = func(err error, context echo.Context) error { httpError := echo.NewHTTPError(http.StatusTeapot, "custom") httpError.Internal = err return httpError } }, expectHandlerCalled: false, expectError: "code=418, message=custom, internal=missing key in request header", }, { name: "nok, custom errorHandler, error from validator", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bearer error-key") }, whenConfig: func(conf *KeyAuthConfig) { conf.ErrorHandler = func(err error, context echo.Context) error { httpError := echo.NewHTTPError(http.StatusTeapot, "custom") httpError.Internal = err return httpError } }, expectHandlerCalled: false, expectError: "code=418, message=custom, internal=some user defined error", }, { name: "nok, defaults, error from validator", givenRequest: func(req *http.Request) { req.Header.Set(echo.HeaderAuthorization, "Bearer error-key") }, whenConfig: func(conf *KeyAuthConfig) {}, expectHandlerCalled: false, expectError: "code=401, message=Unauthorized, internal=some user defined error", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { handlerCalled := false handler := func(c echo.Context) error { handlerCalled = true return c.String(http.StatusOK, "test") } config := KeyAuthConfig{ Validator: testKeyValidator, } if tc.whenConfig != nil { tc.whenConfig(&config) } middlewareChain := KeyAuthWithConfig(config)(handler) e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenRequestFunc != nil { req = tc.givenRequestFunc() } if tc.givenRequest != nil { tc.givenRequest(req) } rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := middlewareChain(c) assert.Equal(t, tc.expectHandlerCalled, handlerCalled) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestKeyAuthWithConfig_panicsOnInvalidLookup(t *testing.T) { assert.PanicsWithError( t, "extractor source for lookup could not be split into needed parts: a", func() { handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } KeyAuthWithConfig(KeyAuthConfig{ Validator: testKeyValidator, KeyLookup: "a", })(handler) }, ) } func TestKeyAuthWithConfig_panicsOnEmptyValidator(t *testing.T) { assert.PanicsWithValue( t, "echo: key-auth middleware requires a validator function", func() { handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } KeyAuthWithConfig(KeyAuthConfig{ Validator: nil, })(handler) }, ) } func TestKeyAuthWithConfig_ContinueOnIgnoredError(t *testing.T) { var testCases = []struct { name string whenContinueOnIgnoredError bool givenKey string expectStatus int expectBody string }{ { name: "no error handler is called", whenContinueOnIgnoredError: true, givenKey: "valid-key", expectStatus: http.StatusTeapot, expectBody: "", }, { name: "ContinueOnIgnoredError is false and error handler is called for missing token", whenContinueOnIgnoredError: false, givenKey: "", // empty response with 200. This emulates previous behaviour when error handler swallowed the error expectStatus: http.StatusOK, expectBody: "", }, { name: "error handler is called for missing token", whenContinueOnIgnoredError: true, givenKey: "", expectStatus: http.StatusTeapot, expectBody: "public-auth", }, { name: "error handler is called for invalid token", whenContinueOnIgnoredError: true, givenKey: "x.x.x", expectStatus: http.StatusUnauthorized, expectBody: "{\"message\":\"Unauthorized\"}\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.GET("/", func(c echo.Context) error { testValue, _ := c.Get("test").(string) return c.String(http.StatusTeapot, testValue) }) e.Use(KeyAuthWithConfig(KeyAuthConfig{ Validator: testKeyValidator, ErrorHandler: func(err error, c echo.Context) error { if _, ok := err.(*ErrKeyAuthMissing); ok { c.Set("test", "public-auth") return nil } return echo.ErrUnauthorized }, KeyLookup: "header:X-API-Key", ContinueOnIgnoredError: tc.whenContinueOnIgnoredError, })) req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.givenKey != "" { req.Header.Set("X-API-Key", tc.givenKey) } res := httptest.NewRecorder() e.ServeHTTP(res, req) assert.Equal(t, tc.expectStatus, res.Code) assert.Equal(t, tc.expectBody, res.Body.String()) }) } } golang-github-labstack-echo-4.11.1/middleware/logger.go000066400000000000000000000153001445647333300227610ustar00rootroot00000000000000package middleware import ( "bytes" "encoding/json" "io" "strconv" "strings" "sync" "time" "github.com/labstack/echo/v4" "github.com/labstack/gommon/color" "github.com/valyala/fasttemplate" ) type ( // LoggerConfig defines the config for Logger middleware. LoggerConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Tags to construct the logger format. // // - time_unix // - time_unix_milli // - time_unix_micro // - time_unix_nano // - time_rfc3339 // - time_rfc3339_nano // - time_custom // - id (Request ID) // - remote_ip // - uri // - host // - method // - path // - route // - protocol // - referer // - user_agent // - status // - error // - latency (In nanoseconds) // - latency_human (Human readable) // - bytes_in (Bytes received) // - bytes_out (Bytes sent) // - header: // - query: // - form: // - custom (see CustomTagFunc field) // // Example "${remote_ip} ${status}" // // Optional. Default value DefaultLoggerConfig.Format. Format string `yaml:"format"` // Optional. Default value DefaultLoggerConfig.CustomTimeFormat. CustomTimeFormat string `yaml:"custom_time_format"` // CustomTagFunc is function called for `${custom}` tag to output user implemented text by writing it to buf. // Make sure that outputted text creates valid JSON string with other logged tags. // Optional. CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error) // Output is a writer where logs in JSON format are written. // Optional. Default value os.Stdout. Output io.Writer template *fasttemplate.Template colorer *color.Color pool *sync.Pool } ) var ( // DefaultLoggerConfig is the default Logger middleware config. DefaultLoggerConfig = LoggerConfig{ Skipper: DefaultSkipper, Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` + `,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", CustomTimeFormat: "2006-01-02 15:04:05.00000", colorer: color.New(), } ) // Logger returns a middleware that logs HTTP requests. func Logger() echo.MiddlewareFunc { return LoggerWithConfig(DefaultLoggerConfig) } // LoggerWithConfig returns a Logger middleware with config. // See: `Logger()`. func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultLoggerConfig.Skipper } if config.Format == "" { config.Format = DefaultLoggerConfig.Format } if config.Output == nil { config.Output = DefaultLoggerConfig.Output } config.template = fasttemplate.New(config.Format, "${", "}") config.colorer = color.New() config.colorer.SetOutput(config.Output) config.pool = &sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, 256)) }, } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { if config.Skipper(c) { return next(c) } req := c.Request() res := c.Response() start := time.Now() if err = next(c); err != nil { c.Error(err) } stop := time.Now() buf := config.pool.Get().(*bytes.Buffer) buf.Reset() defer config.pool.Put(buf) if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) { switch tag { case "custom": if config.CustomTagFunc == nil { return 0, nil } return config.CustomTagFunc(c, buf) case "time_unix": return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10)) case "time_unix_milli": // go 1.17 or later, it supports time#UnixMilli() return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000000, 10)) case "time_unix_micro": // go 1.17 or later, it supports time#UnixMicro() return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000, 10)) case "time_unix_nano": return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10)) case "time_rfc3339": return buf.WriteString(time.Now().Format(time.RFC3339)) case "time_rfc3339_nano": return buf.WriteString(time.Now().Format(time.RFC3339Nano)) case "time_custom": return buf.WriteString(time.Now().Format(config.CustomTimeFormat)) case "id": id := req.Header.Get(echo.HeaderXRequestID) if id == "" { id = res.Header().Get(echo.HeaderXRequestID) } return buf.WriteString(id) case "remote_ip": return buf.WriteString(c.RealIP()) case "host": return buf.WriteString(req.Host) case "uri": return buf.WriteString(req.RequestURI) case "method": return buf.WriteString(req.Method) case "path": p := req.URL.Path if p == "" { p = "/" } return buf.WriteString(p) case "route": return buf.WriteString(c.Path()) case "protocol": return buf.WriteString(req.Proto) case "referer": return buf.WriteString(req.Referer()) case "user_agent": return buf.WriteString(req.UserAgent()) case "status": n := res.Status s := config.colorer.Green(n) switch { case n >= 500: s = config.colorer.Red(n) case n >= 400: s = config.colorer.Yellow(n) case n >= 300: s = config.colorer.Cyan(n) } return buf.WriteString(s) case "error": if err != nil { // Error may contain invalid JSON e.g. `"` b, _ := json.Marshal(err.Error()) b = b[1 : len(b)-1] return buf.Write(b) } case "latency": l := stop.Sub(start) return buf.WriteString(strconv.FormatInt(int64(l), 10)) case "latency_human": return buf.WriteString(stop.Sub(start).String()) case "bytes_in": cl := req.Header.Get(echo.HeaderContentLength) if cl == "" { cl = "0" } return buf.WriteString(cl) case "bytes_out": return buf.WriteString(strconv.FormatInt(res.Size, 10)) default: switch { case strings.HasPrefix(tag, "header:"): return buf.Write([]byte(c.Request().Header.Get(tag[7:]))) case strings.HasPrefix(tag, "query:"): return buf.Write([]byte(c.QueryParam(tag[6:]))) case strings.HasPrefix(tag, "form:"): return buf.Write([]byte(c.FormValue(tag[5:]))) case strings.HasPrefix(tag, "cookie:"): cookie, err := c.Cookie(tag[7:]) if err == nil { return buf.Write([]byte(cookie.Value)) } } } return 0, nil }); err != nil { return } if config.Output == nil { _, err = c.Logger().Output().Write(buf.Bytes()) return } _, err = config.Output.Write(buf.Bytes()) return } } } golang-github-labstack-echo-4.11.1/middleware/logger_test.go000066400000000000000000000230341445647333300240230ustar00rootroot00000000000000package middleware import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "time" "unsafe" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestLogger(t *testing.T) { // Note: Just for the test coverage, not a real test. e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Logger()(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) // Status 2xx h(c) // Status 3xx rec = httptest.NewRecorder() c = e.NewContext(req, rec) h = Logger()(func(c echo.Context) error { return c.String(http.StatusTemporaryRedirect, "test") }) h(c) // Status 4xx rec = httptest.NewRecorder() c = e.NewContext(req, rec) h = Logger()(func(c echo.Context) error { return c.String(http.StatusNotFound, "test") }) h(c) // Status 5xx with empty path req = httptest.NewRequest(http.MethodGet, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h = Logger()(func(c echo.Context) error { return errors.New("error") }) h(c) } func TestLoggerIPAddress(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) buf := new(bytes.Buffer) e.Logger.SetOutput(buf) ip := "127.0.0.1" h := Logger()(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) // With X-Real-IP req.Header.Add(echo.HeaderXRealIP, ip) h(c) assert.Contains(t, buf.String(), ip) // With X-Forwarded-For buf.Reset() req.Header.Del(echo.HeaderXRealIP) req.Header.Add(echo.HeaderXForwardedFor, ip) h(c) assert.Contains(t, buf.String(), ip) buf.Reset() h(c) assert.Contains(t, buf.String(), ip) } func TestLoggerTemplate(t *testing.T) { buf := new(bytes.Buffer) e := echo.New() e.Use(LoggerWithConfig(LoggerConfig{ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "route":"${route}", "referer":"${referer}",` + `"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}", "protocol":"${protocol}"` + `"us":"${query:username}", "cf":"${form:username}", "session":"${cookie:session}"}` + "\n", Output: buf, })) e.GET("/users/:id", func(c echo.Context) error { return c.String(http.StatusOK, "Header Logged") }) req := httptest.NewRequest(http.MethodGet, "/users/1?username=apagano-param&password=secret", nil) req.RequestURI = "/" req.Header.Add(echo.HeaderXRealIP, "127.0.0.1") req.Header.Add("Referer", "google.com") req.Header.Add("User-Agent", "echo-tests-agent") req.Header.Add("X-Custom-Header", "AAA-CUSTOM-VALUE") req.Header.Add("X-Request-ID", "6ba7b810-9dad-11d1-80b4-00c04fd430c8") req.Header.Add("Cookie", "_ga=GA1.2.000000000.0000000000; session=ac08034cd216a647fc2eb62f2bcf7b810") req.Form = url.Values{ "username": []string{"apagano-form"}, "password": []string{"secret-form"}, } rec := httptest.NewRecorder() e.ServeHTTP(rec, req) cases := map[string]bool{ "apagano-param": true, "apagano-form": true, "AAA-CUSTOM-VALUE": true, "BBB-CUSTOM-VALUE": false, "secret-form": false, "hexvalue": false, "GET": true, "127.0.0.1": true, "\"path\":\"/users/1\"": true, "\"route\":\"/users/:id\"": true, "\"uri\":\"/\"": true, "\"status\":200": true, "\"bytes_in\":0": true, "google.com": true, "echo-tests-agent": true, "6ba7b810-9dad-11d1-80b4-00c04fd430c8": true, "ac08034cd216a647fc2eb62f2bcf7b810": true, } for token, present := range cases { assert.True(t, strings.Contains(buf.String(), token) == present, "Case: "+token) } } func TestLoggerCustomTimestamp(t *testing.T) { buf := new(bytes.Buffer) customTimeFormat := "2006-01-02 15:04:05.00000" e := echo.New() e.Use(LoggerWithConfig(LoggerConfig{ Format: `{"time":"${time_custom}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` + `"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}",` + `"us":"${query:username}", "cf":"${form:username}", "session":"${cookie:session}"}` + "\n", CustomTimeFormat: customTimeFormat, Output: buf, })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "custom time stamp test") }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) var objs map[string]*json.RawMessage if err := json.Unmarshal(buf.Bytes(), &objs); err != nil { panic(err) } loggedTime := *(*string)(unsafe.Pointer(objs["time"])) _, err := time.Parse(customTimeFormat, loggedTime) assert.Error(t, err) } func TestLoggerCustomTagFunc(t *testing.T) { e := echo.New() buf := new(bytes.Buffer) e.Use(LoggerWithConfig(LoggerConfig{ Format: `{"method":"${method}",${custom}}` + "\n", CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) { return buf.WriteString(`"tag":"my-value"`) }, Output: buf, })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "custom time stamp test") }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, `{"method":"GET","tag":"my-value"}`+"\n", buf.String()) } func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) { e := echo.New() buf := new(bytes.Buffer) mw := LoggerWithConfig(LoggerConfig{ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` + `"bytes_out":${bytes_out}, "protocol":"${protocol}"}` + "\n", Output: buf, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") c.FormValue("to force parse form") return c.String(http.StatusTeapot, "OK") }) f := make(url.Values) f.Set("csrf", "token") f.Add("multiple", "1") f.Add("multiple", "2") req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode())) req.Header.Set("Referer", "https://echo.labstack.com/") req.Header.Set("User-Agent", "curl/7.68.0") req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw(c) buf.Reset() } } func BenchmarkLoggerWithConfig_withMapFields(b *testing.B) { e := echo.New() buf := new(bytes.Buffer) mw := LoggerWithConfig(LoggerConfig{ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` + `"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}", "protocol":"${protocol}"` + `"us":"${query:username}", "cf":"${form:csrf}", "Referer2":"${header:Referer}"}` + "\n", Output: buf, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") c.FormValue("to force parse form") return c.String(http.StatusTeapot, "OK") }) f := make(url.Values) f.Set("csrf", "token") f.Add("multiple", "1") f.Add("multiple", "2") req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode())) req.Header.Set("Referer", "https://echo.labstack.com/") req.Header.Set("User-Agent", "curl/7.68.0") req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw(c) buf.Reset() } } func TestLoggerTemplateWithTimeUnixMilli(t *testing.T) { buf := new(bytes.Buffer) e := echo.New() e.Use(LoggerWithConfig(LoggerConfig{ Format: `${time_unix_milli}`, Output: buf, })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "OK") }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) unixMillis, err := strconv.ParseInt(buf.String(), 10, 64) assert.NoError(t, err) assert.WithinDuration(t, time.Unix(unixMillis/1000, 0), time.Now(), 3*time.Second) } func TestLoggerTemplateWithTimeUnixMicro(t *testing.T) { buf := new(bytes.Buffer) e := echo.New() e.Use(LoggerWithConfig(LoggerConfig{ Format: `${time_unix_micro}`, Output: buf, })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "OK") }) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) unixMicros, err := strconv.ParseInt(buf.String(), 10, 64) assert.NoError(t, err) assert.WithinDuration(t, time.Unix(unixMicros/1000000, 0), time.Now(), 3*time.Second) } golang-github-labstack-echo-4.11.1/middleware/method_override.go000066400000000000000000000050001445647333300246550ustar00rootroot00000000000000package middleware import ( "net/http" "github.com/labstack/echo/v4" ) type ( // MethodOverrideConfig defines the config for MethodOverride middleware. MethodOverrideConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Getter is a function that gets overridden method from the request. // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). Getter MethodOverrideGetter } // MethodOverrideGetter is a function that gets overridden method from the request MethodOverrideGetter func(echo.Context) string ) var ( // DefaultMethodOverrideConfig is the default MethodOverride middleware config. DefaultMethodOverrideConfig = MethodOverrideConfig{ Skipper: DefaultSkipper, Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), } ) // MethodOverride returns a MethodOverride middleware. // MethodOverride middleware checks for the overridden method from the request and // uses it instead of the original method. // // For security reasons, only `POST` method can be overridden. func MethodOverride() echo.MiddlewareFunc { return MethodOverrideWithConfig(DefaultMethodOverrideConfig) } // MethodOverrideWithConfig returns a MethodOverride middleware with config. // See: `MethodOverride()`. func MethodOverrideWithConfig(config MethodOverrideConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultMethodOverrideConfig.Skipper } if config.Getter == nil { config.Getter = DefaultMethodOverrideConfig.Getter } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() if req.Method == http.MethodPost { m := config.Getter(c) if m != "" { req.Method = m } } return next(c) } } } // MethodFromHeader is a `MethodOverrideGetter` that gets overridden method from // the request header. func MethodFromHeader(header string) MethodOverrideGetter { return func(c echo.Context) string { return c.Request().Header.Get(header) } } // MethodFromForm is a `MethodOverrideGetter` that gets overridden method from the // form parameter. func MethodFromForm(param string) MethodOverrideGetter { return func(c echo.Context) string { return c.FormValue(param) } } // MethodFromQuery is a `MethodOverrideGetter` that gets overridden method from // the query parameter. func MethodFromQuery(param string) MethodOverrideGetter { return func(c echo.Context) string { return c.QueryParam(param) } } golang-github-labstack-echo-4.11.1/middleware/method_override_test.go000066400000000000000000000027261445647333300257300ustar00rootroot00000000000000package middleware import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestMethodOverride(t *testing.T) { e := echo.New() m := MethodOverride() h := func(c echo.Context) error { return c.String(http.StatusOK, "test") } // Override with http header req := httptest.NewRequest(http.MethodPost, "/", nil) rec := httptest.NewRecorder() req.Header.Set(echo.HeaderXHTTPMethodOverride, http.MethodDelete) c := e.NewContext(req, rec) m(h)(c) assert.Equal(t, http.MethodDelete, req.Method) // Override with form parameter m = MethodOverrideWithConfig(MethodOverrideConfig{Getter: MethodFromForm("_method")}) req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader([]byte("_method="+http.MethodDelete))) rec = httptest.NewRecorder() req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) c = e.NewContext(req, rec) m(h)(c) assert.Equal(t, http.MethodDelete, req.Method) // Override with query parameter m = MethodOverrideWithConfig(MethodOverrideConfig{Getter: MethodFromQuery("_method")}) req = httptest.NewRequest(http.MethodPost, "/?_method="+http.MethodDelete, nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) m(h)(c) assert.Equal(t, http.MethodDelete, req.Method) // Ignore `GET` req = httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderXHTTPMethodOverride, http.MethodDelete) assert.Equal(t, http.MethodGet, req.Method) } golang-github-labstack-echo-4.11.1/middleware/middleware.go000066400000000000000000000041261445647333300236230ustar00rootroot00000000000000package middleware import ( "net/http" "regexp" "strconv" "strings" "github.com/labstack/echo/v4" ) type ( // Skipper defines a function to skip middleware. Returning true skips processing // the middleware. Skipper func(c echo.Context) bool // BeforeFunc defines a function which is executed just before the middleware. BeforeFunc func(c echo.Context) ) func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer { groups := pattern.FindAllStringSubmatch(input, -1) if groups == nil { return nil } values := groups[0][1:] replace := make([]string, 2*len(values)) for i, v := range values { j := 2 * i replace[j] = "$" + strconv.Itoa(i+1) replace[j+1] = v } return strings.NewReplacer(replace...) } func rewriteRulesRegex(rewrite map[string]string) map[*regexp.Regexp]string { // Initialize rulesRegex := map[*regexp.Regexp]string{} for k, v := range rewrite { k = regexp.QuoteMeta(k) k = strings.ReplaceAll(k, `\*`, "(.*?)") if strings.HasPrefix(k, `\^`) { k = strings.ReplaceAll(k, `\^`, "^") } k = k + "$" rulesRegex[regexp.MustCompile(k)] = v } return rulesRegex } func rewriteURL(rewriteRegex map[*regexp.Regexp]string, req *http.Request) error { if len(rewriteRegex) == 0 { return nil } // Depending how HTTP request is sent RequestURI could contain Scheme://Host/path or be just /path. // We only want to use path part for rewriting and therefore trim prefix if it exists rawURI := req.RequestURI if rawURI != "" && rawURI[0] != '/' { prefix := "" if req.URL.Scheme != "" { prefix = req.URL.Scheme + "://" } if req.URL.Host != "" { prefix += req.URL.Host // host or host:port } if prefix != "" { rawURI = strings.TrimPrefix(rawURI, prefix) } } for k, v := range rewriteRegex { if replacer := captureTokens(k, rawURI); replacer != nil { url, err := req.URL.Parse(replacer.Replace(v)) if err != nil { return err } req.URL = url return nil // rewrite only once } } return nil } // DefaultSkipper returns false which processes the middleware. func DefaultSkipper(echo.Context) bool { return false } golang-github-labstack-echo-4.11.1/middleware/middleware_test.go000066400000000000000000000051611445647333300246620ustar00rootroot00000000000000package middleware import ( "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "regexp" "testing" ) func TestRewriteURL(t *testing.T) { var testCases = []struct { whenURL string expectPath string expectRawPath string expectQuery string expectErr string }{ { whenURL: "http://localhost:8080/old", expectPath: "/new", expectRawPath: "", }, { // encoded `ol%64` (decoded `old`) should not be rewritten to `/new` whenURL: "/ol%64", // `%64` is decoded `d` expectPath: "/old", expectRawPath: "/ol%64", }, { whenURL: "http://localhost:8080/users/+_+/orders/___++++?test=1", expectPath: "/user/+_+/order/___++++", expectRawPath: "", expectQuery: "test=1", }, { whenURL: "http://localhost:8080/users/%20a/orders/%20aa", expectPath: "/user/ a/order/ aa", expectRawPath: "", }, { whenURL: "http://localhost:8080/%47%6f%2f?test=1", expectPath: "/Go/", expectRawPath: "/%47%6f%2f", expectQuery: "test=1", }, { whenURL: "/users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F", expectPath: "/user/jill/order/T/cO4lW/t/Vp/", expectRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", }, { // do nothing, replace nothing whenURL: "http://localhost:8080/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", expectPath: "/user/jill/order/T/cO4lW/t/Vp/", expectRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", }, { whenURL: "http://localhost:8080/static", expectPath: "/static/path", expectRawPath: "", expectQuery: "role=AUTHOR&limit=1000", }, { whenURL: "/static", expectPath: "/static/path", expectRawPath: "", expectQuery: "role=AUTHOR&limit=1000", }, } rules := map[*regexp.Regexp]string{ regexp.MustCompile("^/old$"): "/new", regexp.MustCompile("^/users/(.*?)/orders/(.*?)$"): "/user/$1/order/$2", regexp.MustCompile("^/static$"): "/static/path?role=AUTHOR&limit=1000", } for _, tc := range testCases { t.Run(tc.whenURL, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) err := rewriteURL(rules, req) if tc.expectErr != "" { assert.EqualError(t, err, tc.expectErr) } else { assert.NoError(t, err) } assert.Equal(t, tc.expectPath, req.URL.Path) // Path field is stored in decoded form: /%47%6f%2f becomes /Go/. assert.Equal(t, tc.expectRawPath, req.URL.RawPath) // RawPath, an optional field which only gets set if the default encoding is different from Path. assert.Equal(t, tc.expectQuery, req.URL.RawQuery) }) } } golang-github-labstack-echo-4.11.1/middleware/proxy.go000066400000000000000000000316571445647333300227000ustar00rootroot00000000000000package middleware import ( "context" "fmt" "io" "math/rand" "net" "net/http" "net/http/httputil" "net/url" "regexp" "strings" "sync" "time" "github.com/labstack/echo/v4" ) // TODO: Handle TLS proxy type ( // ProxyConfig defines the config for Proxy middleware. ProxyConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Balancer defines a load balancing technique. // Required. Balancer ProxyBalancer // RetryCount defines the number of times a failed proxied request should be retried // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. RetryCount int // RetryFilter defines a function used to determine if a failed request to a // ProxyTarget should be retried. The RetryFilter will only be called when the number // of previous retries is less than RetryCount. If the function returns true, the // request will be retried. The provided error indicates the reason for the request // failure. When the ProxyTarget is unavailable, the error will be an instance of // echo.HTTPError with a Code of http.StatusBadGateway. In all other cases, the error // will indicate an internal error in the Proxy middleware. When a RetryFilter is not // specified, all requests that fail with http.StatusBadGateway will be retried. A custom // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is // only called when the request to the target fails, or an internal error in the Proxy // middleware has occurred. Successful requests that return a non-200 response code cannot // be retried. RetryFilter func(c echo.Context, e error) bool // ErrorHandler defines a function which can be used to return custom errors from // the Proxy middleware. ErrorHandler is only invoked when there has been // either an internal error in the Proxy middleware or the ProxyTarget is // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked // when a ProxyTarget returns a non-200 response. In these cases, the response // is already written so errors cannot be modified. ErrorHandler is only // invoked after all retry attempts have been exhausted. ErrorHandler func(c echo.Context, err error) error // Rewrite defines URL path rewrite rules. The values captured in asterisk can be // retrieved by index e.g. $1, $2 and so on. // Examples: // "/old": "/new", // "/api/*": "/$1", // "/js/*": "/public/javascripts/$1", // "/users/*/orders/*": "/user/$1/order/$2", Rewrite map[string]string // RegexRewrite defines rewrite rules using regexp.Rexexp with captures // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. // Example: // "^/old/[0.9]+/": "/new", // "^/api/.+?/(.*)": "/v2/$1", RegexRewrite map[*regexp.Regexp]string // Context key to store selected ProxyTarget into context. // Optional. Default value "target". ContextKey string // To customize the transport to remote. // Examples: If custom TLS certificates are required. Transport http.RoundTripper // ModifyResponse defines function to modify response from ProxyTarget. ModifyResponse func(*http.Response) error } // ProxyTarget defines the upstream target. ProxyTarget struct { Name string URL *url.URL Meta echo.Map } // ProxyBalancer defines an interface to implement a load balancing technique. ProxyBalancer interface { AddTarget(*ProxyTarget) bool RemoveTarget(string) bool Next(echo.Context) *ProxyTarget } // TargetProvider defines an interface that gives the opportunity for balancer // to return custom errors when selecting target. TargetProvider interface { NextTarget(echo.Context) (*ProxyTarget, error) } commonBalancer struct { targets []*ProxyTarget mutex sync.Mutex } // RandomBalancer implements a random load balancing technique. randomBalancer struct { commonBalancer random *rand.Rand } // RoundRobinBalancer implements a round-robin load balancing technique. roundRobinBalancer struct { commonBalancer // tracking the index on `targets` slice for the next `*ProxyTarget` to be used i int } ) var ( // DefaultProxyConfig is the default Proxy middleware config. DefaultProxyConfig = ProxyConfig{ Skipper: DefaultSkipper, ContextKey: "target", } ) func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { in, _, err := c.Response().Hijack() if err != nil { c.Set("_error", fmt.Errorf("proxy raw, hijack error=%w, url=%s", err, t.URL)) return } defer in.Close() out, err := net.Dial("tcp", t.URL.Host) if err != nil { c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", err, t.URL))) return } defer out.Close() // Write header err = r.Write(out) if err != nil { c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", err, t.URL))) return } errCh := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { _, err = io.Copy(dst, src) errCh <- err } go cp(out, in) go cp(in, out) err = <-errCh if err != nil && err != io.EOF { c.Set("_error", fmt.Errorf("proxy raw, copy body error=%w, url=%s", err, t.URL)) } }) } // NewRandomBalancer returns a random proxy balancer. func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer { b := randomBalancer{} b.targets = targets b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) return &b } // NewRoundRobinBalancer returns a round-robin proxy balancer. func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer { b := roundRobinBalancer{} b.targets = targets return &b } // AddTarget adds an upstream target to the list and returns `true`. // // However, if a target with the same name already exists then the operation is aborted returning `false`. func (b *commonBalancer) AddTarget(target *ProxyTarget) bool { b.mutex.Lock() defer b.mutex.Unlock() for _, t := range b.targets { if t.Name == target.Name { return false } } b.targets = append(b.targets, target) return true } // RemoveTarget removes an upstream target from the list by name. // // Returns `true` on success, `false` if no target with the name is found. func (b *commonBalancer) RemoveTarget(name string) bool { b.mutex.Lock() defer b.mutex.Unlock() for i, t := range b.targets { if t.Name == name { b.targets = append(b.targets[:i], b.targets[i+1:]...) return true } } return false } // Next randomly returns an upstream target. // // Note: `nil` is returned in case upstream target list is empty. func (b *randomBalancer) Next(c echo.Context) *ProxyTarget { b.mutex.Lock() defer b.mutex.Unlock() if len(b.targets) == 0 { return nil } else if len(b.targets) == 1 { return b.targets[0] } return b.targets[b.random.Intn(len(b.targets))] } // Next returns an upstream target using round-robin technique. In the case // where a previously failed request is being retried, the round-robin // balancer will attempt to use the next target relative to the original // request. If the list of targets held by the balancer is modified while a // failed request is being retried, it is possible that the balancer will // return the original failed target. // // Note: `nil` is returned in case upstream target list is empty. func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget { b.mutex.Lock() defer b.mutex.Unlock() if len(b.targets) == 0 { return nil } else if len(b.targets) == 1 { return b.targets[0] } var i int const lastIdxKey = "_round_robin_last_index" // This request is a retry, start from the index of the previous // target to ensure we don't attempt to retry the request with // the same failed target if c.Get(lastIdxKey) != nil { i = c.Get(lastIdxKey).(int) i++ if i >= len(b.targets) { i = 0 } } else { // This is a first time request, use the global index if b.i >= len(b.targets) { b.i = 0 } i = b.i b.i++ } c.Set(lastIdxKey, i) return b.targets[i] } // Proxy returns a Proxy middleware. // // Proxy middleware forwards the request to upstream server using a configured load balancing technique. func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc { c := DefaultProxyConfig c.Balancer = balancer return ProxyWithConfig(c) } // ProxyWithConfig returns a Proxy middleware with config. // See: `Proxy()` func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc { if config.Balancer == nil { panic("echo: proxy middleware requires balancer") } // Defaults if config.Skipper == nil { config.Skipper = DefaultProxyConfig.Skipper } if config.RetryFilter == nil { config.RetryFilter = func(c echo.Context, e error) bool { if httpErr, ok := e.(*echo.HTTPError); ok { return httpErr.Code == http.StatusBadGateway } return false } } if config.ErrorHandler == nil { config.ErrorHandler = func(c echo.Context, err error) error { return err } } if config.Rewrite != nil { if config.RegexRewrite == nil { config.RegexRewrite = make(map[*regexp.Regexp]string) } for k, v := range rewriteRulesRegex(config.Rewrite) { config.RegexRewrite[k] = v } } provider, isTargetProvider := config.Balancer.(TargetProvider) return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() res := c.Response() if err := rewriteURL(config.RegexRewrite, req); err != nil { return config.ErrorHandler(c, err) } // Fix header // Basically it's not good practice to unconditionally pass incoming x-real-ip header to upstream. // However, for backward compatibility, legacy behavior is preserved unless you configure Echo#IPExtractor. if req.Header.Get(echo.HeaderXRealIP) == "" || c.Echo().IPExtractor != nil { req.Header.Set(echo.HeaderXRealIP, c.RealIP()) } if req.Header.Get(echo.HeaderXForwardedProto) == "" { req.Header.Set(echo.HeaderXForwardedProto, c.Scheme()) } if c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == "" { // For HTTP, it is automatically set by Go HTTP reverse proxy. req.Header.Set(echo.HeaderXForwardedFor, c.RealIP()) } retries := config.RetryCount for { var tgt *ProxyTarget var err error if isTargetProvider { tgt, err = provider.NextTarget(c) if err != nil { return config.ErrorHandler(c, err) } } else { tgt = config.Balancer.Next(c) } c.Set(config.ContextKey, tgt) //If retrying a failed request, clear any previous errors from //context here so that balancers have the option to check for //errors that occurred using previous target if retries < config.RetryCount { c.Set("_error", nil) } // Proxy switch { case c.IsWebSocket(): proxyRaw(tgt, c).ServeHTTP(res, req) case req.Header.Get(echo.HeaderAccept) == "text/event-stream": default: proxyHTTP(tgt, c, config).ServeHTTP(res, req) } err, hasError := c.Get("_error").(error) if !hasError { return nil } retry := retries > 0 && config.RetryFilter(c, err) if !retry { return config.ErrorHandler(c, err) } retries-- } } } } // StatusCodeContextCanceled is a custom HTTP status code for situations // where a client unexpectedly closed the connection to the server. // As there is no standard error code for "client closed connection", but // various well-known HTTP clients and server implement this HTTP code we use // 499 too instead of the more problematic 5xx, which does not allow to detect this situation const StatusCodeContextCanceled = 499 func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler { proxy := httputil.NewSingleHostReverseProxy(tgt.URL) proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) { desc := tgt.URL.String() if tgt.Name != "" { desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String()) } // If the client canceled the request (usually by closing the connection), we can report a // client error (4xx) instead of a server error (5xx) to correctly identify the situation. // The Go standard library (at of late 2020) wraps the exported, standard // context.Canceled error with unexported garbage value requiring a substring check, see // https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430 if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") { httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err)) httpError.Internal = err c.Set("_error", httpError) } else { httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err)) httpError.Internal = err c.Set("_error", httpError) } } proxy.Transport = config.Transport proxy.ModifyResponse = config.ModifyResponse return proxy } golang-github-labstack-echo-4.11.1/middleware/proxy_test.go000066400000000000000000000462361445647333300237360ustar00rootroot00000000000000package middleware import ( "bytes" "context" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "regexp" "sync" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) // Assert expected with url.EscapedPath method to obtain the path. func TestProxy(t *testing.T) { // Setup t1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "target 1") })) defer t1.Close() url1, _ := url.Parse(t1.URL) t2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "target 2") })) defer t2.Close() url2, _ := url.Parse(t2.URL) targets := []*ProxyTarget{ { Name: "target 1", URL: url1, }, { Name: "target 2", URL: url2, }, } rb := NewRandomBalancer(nil) // must add targets: for _, target := range targets { assert.True(t, rb.AddTarget(target)) } // must ignore duplicates: for _, target := range targets { assert.False(t, rb.AddTarget(target)) } // Random e := echo.New() e.Use(Proxy(rb)) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) body := rec.Body.String() expected := map[string]bool{ "target 1": true, "target 2": true, } assert.Condition(t, func() bool { return expected[body] }) for _, target := range targets { assert.True(t, rb.RemoveTarget(target.Name)) } assert.False(t, rb.RemoveTarget("unknown target")) // Round-robin rrb := NewRoundRobinBalancer(targets) e = echo.New() e.Use(Proxy(rrb)) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) body = rec.Body.String() assert.Equal(t, "target 1", body) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) body = rec.Body.String() assert.Equal(t, "target 2", body) // ModifyResponse e = echo.New() e.Use(ProxyWithConfig(ProxyConfig{ Balancer: rrb, ModifyResponse: func(res *http.Response) error { res.Body = io.NopCloser(bytes.NewBuffer([]byte("modified"))) res.Header.Set("X-Modified", "1") return nil }, })) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "modified", rec.Body.String()) assert.Equal(t, "1", rec.Header().Get("X-Modified")) // ProxyTarget is set in context contextObserver := func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { next(c) assert.Contains(t, targets, c.Get("target"), "target is not set in context") return nil } } rrb1 := NewRoundRobinBalancer(targets) e = echo.New() e.Use(contextObserver) e.Use(Proxy(rrb1)) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) } type testProvider struct { commonBalancer target *ProxyTarget err error } func (p *testProvider) Next(c echo.Context) *ProxyTarget { return &ProxyTarget{} } func (p *testProvider) NextTarget(c echo.Context) (*ProxyTarget, error) { return p.target, p.err } func TestTargetProvider(t *testing.T) { t1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "target 1") })) defer t1.Close() url1, _ := url.Parse(t1.URL) e := echo.New() tp := &testProvider{} tp.target = &ProxyTarget{Name: "target 1", URL: url1} e.Use(Proxy(tp)) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) e.ServeHTTP(rec, req) body := rec.Body.String() assert.Equal(t, "target 1", body) } func TestFailNextTarget(t *testing.T) { url1, err := url.Parse("http://dummy:8080") assert.Nil(t, err) e := echo.New() tp := &testProvider{} tp.target = &ProxyTarget{Name: "target 1", URL: url1} tp.err = echo.NewHTTPError(http.StatusInternalServerError, "method could not select target") e.Use(Proxy(tp)) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) e.ServeHTTP(rec, req) body := rec.Body.String() assert.Equal(t, "{\"message\":\"method could not select target\"}\n", body) } func TestProxyRealIPHeader(t *testing.T) { // Setup upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer upstream.Close() url, _ := url.Parse(upstream.URL) rrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: "upstream", URL: url}}) e := echo.New() e.Use(Proxy(rrb)) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() remoteAddrIP, _, _ := net.SplitHostPort(req.RemoteAddr) realIPHeaderIP := "203.0.113.1" extractedRealIP := "203.0.113.10" tests := []*struct { hasRealIPheader bool hasIPExtractor bool extectedXRealIP string }{ {false, false, remoteAddrIP}, {false, true, extractedRealIP}, {true, false, realIPHeaderIP}, {true, true, extractedRealIP}, } for _, tt := range tests { if tt.hasRealIPheader { req.Header.Set(echo.HeaderXRealIP, realIPHeaderIP) } else { req.Header.Del(echo.HeaderXRealIP) } if tt.hasIPExtractor { e.IPExtractor = func(*http.Request) string { return extractedRealIP } } else { e.IPExtractor = nil } e.ServeHTTP(rec, req) assert.Equal(t, tt.extectedXRealIP, req.Header.Get(echo.HeaderXRealIP), "hasRealIPheader: %t / hasIPExtractor: %t", tt.hasRealIPheader, tt.hasIPExtractor) } } func TestProxyRewrite(t *testing.T) { var testCases = []struct { whenPath string expectProxiedURI string expectStatus int }{ { whenPath: "/api/users", expectProxiedURI: "/users", expectStatus: http.StatusOK, }, { whenPath: "/js/main.js", expectProxiedURI: "/public/javascripts/main.js", expectStatus: http.StatusOK, }, { whenPath: "/old", expectProxiedURI: "/new", expectStatus: http.StatusOK, }, { whenPath: "/users/jack/orders/1", expectProxiedURI: "/user/jack/order/1", expectStatus: http.StatusOK, }, { whenPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", expectProxiedURI: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", expectStatus: http.StatusOK, }, { // ` ` (space) is encoded by httpClient to `%20` when doing request to Echo. `%20` should not be double escaped when proxying request whenPath: "/api/new users", expectProxiedURI: "/new%20users", expectStatus: http.StatusOK, }, { // query params should be proxied and not be modified whenPath: "/api/users?limit=10", expectProxiedURI: "/users?limit=10", expectStatus: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenPath, func(t *testing.T) { receivedRequestURI := make(chan string, 1) upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // RequestURI is the unmodified request-target of the Request-Line (RFC 7230, Section 3.1.1) as sent by the client to a server // we need unmodified target to see if we are encoding/decoding the url in addition to rewrite/replace logic // if original request had `%2F` we should not magically decode it to `/` as it would change what was requested receivedRequestURI <- r.RequestURI })) defer upstream.Close() serverURL, _ := url.Parse(upstream.URL) rrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: "upstream", URL: serverURL}}) // Rewrite e := echo.New() e.Use(ProxyWithConfig(ProxyConfig{ Balancer: rrb, Rewrite: map[string]string{ "/old": "/new", "/api/*": "/$1", "/js/*": "/public/javascripts/$1", "/users/*/orders/*": "/user/$1/order/$2", }, })) targetURL, _ := serverURL.Parse(tc.whenPath) req := httptest.NewRequest(http.MethodGet, targetURL.String(), nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectStatus, rec.Code) actualRequestURI := <-receivedRequestURI assert.Equal(t, tc.expectProxiedURI, actualRequestURI) }) } } func TestProxyRewriteRegex(t *testing.T) { // Setup receivedRequestURI := make(chan string, 1) upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // RequestURI is the unmodified request-target of the Request-Line (RFC 7230, Section 3.1.1) as sent by the client to a server // we need unmodified target to see if we are encoding/decoding the url in addition to rewrite/replace logic // if original request had `%2F` we should not magically decode it to `/` as it would change what was requested receivedRequestURI <- r.RequestURI })) defer upstream.Close() tmpUrL, _ := url.Parse(upstream.URL) rrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: "upstream", URL: tmpUrL}}) // Rewrite e := echo.New() e.Use(ProxyWithConfig(ProxyConfig{ Balancer: rrb, Rewrite: map[string]string{ "^/a/*": "/v1/$1", "^/b/*/c/*": "/v2/$2/$1", "^/c/*/*": "/v3/$2", }, RegexRewrite: map[*regexp.Regexp]string{ regexp.MustCompile("^/x/.+?/(.*)"): "/v4/$1", regexp.MustCompile("^/y/(.+?)/(.*)"): "/v5/$2/$1", }, })) testCases := []struct { requestPath string statusCode int expectPath string }{ {"/unmatched", http.StatusOK, "/unmatched"}, {"/a/test", http.StatusOK, "/v1/test"}, {"/b/foo/c/bar/baz", http.StatusOK, "/v2/bar/baz/foo"}, {"/c/ignore/test", http.StatusOK, "/v3/test"}, {"/c/ignore1/test/this", http.StatusOK, "/v3/test/this"}, {"/x/ignore/test", http.StatusOK, "/v4/test"}, {"/y/foo/bar", http.StatusOK, "/v5/bar/foo"}, // NB: fragment is not added by golang httputil.NewSingleHostReverseProxy implementation // $2 = `bar?q=1#frag`, $1 = `foo`. replaced uri = `/v5/bar?q=1#frag/foo` but httputil.NewSingleHostReverseProxy does not send `#frag/foo` (currently) {"/y/foo/bar?q=1#frag", http.StatusOK, "/v5/bar?q=1"}, } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { targetURL, _ := url.Parse(tc.requestPath) req := httptest.NewRequest(http.MethodGet, targetURL.String(), nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) actualRequestURI := <-receivedRequestURI assert.Equal(t, tc.expectPath, actualRequestURI) assert.Equal(t, tc.statusCode, rec.Code) }) } } func TestProxyError(t *testing.T) { // Setup url1, _ := url.Parse("http://127.0.0.1:27121") url2, _ := url.Parse("http://127.0.0.1:27122") targets := []*ProxyTarget{ { Name: "target 1", URL: url1, }, { Name: "target 2", URL: url2, }, } rb := NewRandomBalancer(nil) // must add targets: for _, target := range targets { assert.True(t, rb.AddTarget(target)) } // must ignore duplicates: for _, target := range targets { assert.False(t, rb.AddTarget(target)) } // Random e := echo.New() e.Use(Proxy(rb)) req := httptest.NewRequest(http.MethodGet, "/", nil) // Remote unreachable rec := httptest.NewRecorder() req.URL.Path = "/api/users" e.ServeHTTP(rec, req) assert.Equal(t, "/api/users", req.URL.Path) assert.Equal(t, http.StatusBadGateway, rec.Code) } func TestProxyRetries(t *testing.T) { newServer := func(res int) (*url.URL, *httptest.Server) { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(res) }), ) targetURL, _ := url.Parse(server.URL) return targetURL, server } targetURL, server := newServer(http.StatusOK) defer server.Close() goodTarget := &ProxyTarget{ Name: "Good", URL: targetURL, } targetURL, server = newServer(http.StatusBadRequest) defer server.Close() goodTargetWith40X := &ProxyTarget{ Name: "Good with 40X", URL: targetURL, } targetURL, _ = url.Parse("http://127.0.0.1:27121") badTarget := &ProxyTarget{ Name: "Bad", URL: targetURL, } alwaysRetryFilter := func(c echo.Context, e error) bool { return true } neverRetryFilter := func(c echo.Context, e error) bool { return false } testCases := []struct { name string retryCount int retryFilters []func(c echo.Context, e error) bool targets []*ProxyTarget expectedResponse int }{ { name: "retry count 0 does not attempt retry on fail", targets: []*ProxyTarget{ badTarget, goodTarget, }, expectedResponse: http.StatusBadGateway, }, { name: "retry count 1 does not attempt retry on success", retryCount: 1, targets: []*ProxyTarget{ goodTarget, }, expectedResponse: http.StatusOK, }, { name: "retry count 1 does retry on handler return true", retryCount: 1, retryFilters: []func(c echo.Context, e error) bool{ alwaysRetryFilter, }, targets: []*ProxyTarget{ badTarget, goodTarget, }, expectedResponse: http.StatusOK, }, { name: "retry count 1 does not retry on handler return false", retryCount: 1, retryFilters: []func(c echo.Context, e error) bool{ neverRetryFilter, }, targets: []*ProxyTarget{ badTarget, goodTarget, }, expectedResponse: http.StatusBadGateway, }, { name: "retry count 2 returns error when no more retries left", retryCount: 2, retryFilters: []func(c echo.Context, e error) bool{ alwaysRetryFilter, alwaysRetryFilter, }, targets: []*ProxyTarget{ badTarget, badTarget, badTarget, goodTarget, //Should never be reached as only 2 retries }, expectedResponse: http.StatusBadGateway, }, { name: "retry count 2 returns error when retries left but handler returns false", retryCount: 3, retryFilters: []func(c echo.Context, e error) bool{ alwaysRetryFilter, alwaysRetryFilter, neverRetryFilter, }, targets: []*ProxyTarget{ badTarget, badTarget, badTarget, goodTarget, //Should never be reached as retry handler returns false on 2nd check }, expectedResponse: http.StatusBadGateway, }, { name: "retry count 3 succeeds", retryCount: 3, retryFilters: []func(c echo.Context, e error) bool{ alwaysRetryFilter, alwaysRetryFilter, alwaysRetryFilter, }, targets: []*ProxyTarget{ badTarget, badTarget, badTarget, goodTarget, }, expectedResponse: http.StatusOK, }, { name: "40x responses are not retried", retryCount: 1, targets: []*ProxyTarget{ goodTargetWith40X, goodTarget, }, expectedResponse: http.StatusBadRequest, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { retryFilterCall := 0 retryFilter := func(c echo.Context, e error) bool { if len(tc.retryFilters) == 0 { assert.FailNow(t, fmt.Sprintf("unexpected calls, %d, to retry handler", retryFilterCall)) } retryFilterCall++ nextRetryFilter := tc.retryFilters[0] tc.retryFilters = tc.retryFilters[1:] return nextRetryFilter(c, e) } e := echo.New() e.Use(ProxyWithConfig( ProxyConfig{ Balancer: NewRoundRobinBalancer(tc.targets), RetryCount: tc.retryCount, RetryFilter: retryFilter, }, )) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectedResponse, rec.Code) if len(tc.retryFilters) > 0 { assert.FailNow(t, fmt.Sprintf("expected %d more retry handler calls", len(tc.retryFilters))) } }) } } func TestProxyRetryWithBackendTimeout(t *testing.T) { transport := http.DefaultTransport.(*http.Transport).Clone() transport.ResponseHeaderTimeout = time.Millisecond * 500 timeoutBackend := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(1 * time.Second) w.WriteHeader(404) }), ) defer timeoutBackend.Close() timeoutTargetURL, _ := url.Parse(timeoutBackend.URL) goodBackend := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }), ) defer goodBackend.Close() goodTargetURL, _ := url.Parse(goodBackend.URL) e := echo.New() e.Use(ProxyWithConfig( ProxyConfig{ Transport: transport, Balancer: NewRoundRobinBalancer([]*ProxyTarget{ { Name: "Timeout", URL: timeoutTargetURL, }, { Name: "Good", URL: goodTargetURL, }, }), RetryCount: 1, }, )) var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, 200, rec.Code) }() } wg.Wait() } func TestProxyErrorHandler(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) goodURL, _ := url.Parse(server.URL) defer server.Close() goodTarget := &ProxyTarget{ Name: "Good", URL: goodURL, } badURL, _ := url.Parse("http://127.0.0.1:27121") badTarget := &ProxyTarget{ Name: "Bad", URL: badURL, } transformedError := errors.New("a new error") testCases := []struct { name string target *ProxyTarget errorHandler func(c echo.Context, e error) error expectFinalError func(t *testing.T, err error) }{ { name: "Error handler not invoked when request success", target: goodTarget, errorHandler: func(c echo.Context, e error) error { assert.FailNow(t, "error handler should not be invoked") return e }, }, { name: "Error handler invoked when request fails", target: badTarget, errorHandler: func(c echo.Context, e error) error { httpErr, ok := e.(*echo.HTTPError) assert.True(t, ok, "expected http error to be passed to handler") assert.Equal(t, http.StatusBadGateway, httpErr.Code, "expected http bad gateway error to be passed to handler") return transformedError }, expectFinalError: func(t *testing.T, err error) { assert.Equal(t, transformedError, err, "transformed error not returned from proxy") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() e.Use(ProxyWithConfig( ProxyConfig{ Balancer: NewRoundRobinBalancer([]*ProxyTarget{tc.target}), ErrorHandler: tc.errorHandler, }, )) errorHandlerCalled := false e.HTTPErrorHandler = func(err error, c echo.Context) { errorHandlerCalled = true tc.expectFinalError(t, err) e.DefaultHTTPErrorHandler(err, c) } req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) if !errorHandlerCalled && tc.expectFinalError != nil { t.Fatalf("error handler was not called") } }) } } func TestClientCancelConnectionResultsHTTPCode499(t *testing.T) { var timeoutStop sync.WaitGroup timeoutStop.Add(1) HTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { timeoutStop.Wait() // wait until we have canceled the request w.WriteHeader(http.StatusOK) })) defer HTTPTarget.Close() targetURL, _ := url.Parse(HTTPTarget.URL) target := &ProxyTarget{ Name: "target", URL: targetURL, } rb := NewRandomBalancer(nil) assert.True(t, rb.AddTarget(target)) e := echo.New() e.Use(Proxy(rb)) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) go func() { time.Sleep(10 * time.Millisecond) cancel() }() e.ServeHTTP(rec, req) timeoutStop.Done() assert.Equal(t, 499, rec.Code) } // Assert balancer with empty targets does return `nil` on `Next()` func TestProxyBalancerWithNoTargets(t *testing.T) { rb := NewRandomBalancer(nil) assert.Nil(t, rb.Next(nil)) rrb := NewRoundRobinBalancer([]*ProxyTarget{}) assert.Nil(t, rrb.Next(nil)) } golang-github-labstack-echo-4.11.1/middleware/rate_limiter.go000066400000000000000000000206371445647333300241730ustar00rootroot00000000000000package middleware import ( "net/http" "sync" "time" "github.com/labstack/echo/v4" "golang.org/x/time/rate" ) type ( // RateLimiterStore is the interface to be implemented by custom stores. RateLimiterStore interface { // Stores for the rate limiter have to implement the Allow method Allow(identifier string) (bool, error) } ) type ( // RateLimiterConfig defines the configuration for the rate limiter RateLimiterConfig struct { Skipper Skipper BeforeFunc BeforeFunc // IdentifierExtractor uses echo.Context to extract the identifier for a visitor IdentifierExtractor Extractor // Store defines a store for the rate limiter Store RateLimiterStore // ErrorHandler provides a handler to be called when IdentifierExtractor returns an error ErrorHandler func(context echo.Context, err error) error // DenyHandler provides a handler to be called when RateLimiter denies access DenyHandler func(context echo.Context, identifier string, err error) error } // Extractor is used to extract data from echo.Context Extractor func(context echo.Context) (string, error) ) // errors var ( // ErrRateLimitExceeded denotes an error raised when rate limit is exceeded ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") // ErrExtractorError denotes an error raised when extractor function is unsuccessful ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") ) // DefaultRateLimiterConfig defines default values for RateLimiterConfig var DefaultRateLimiterConfig = RateLimiterConfig{ Skipper: DefaultSkipper, IdentifierExtractor: func(ctx echo.Context) (string, error) { id := ctx.RealIP() return id, nil }, ErrorHandler: func(context echo.Context, err error) error { return &echo.HTTPError{ Code: ErrExtractorError.Code, Message: ErrExtractorError.Message, Internal: err, } }, DenyHandler: func(context echo.Context, identifier string, err error) error { return &echo.HTTPError{ Code: ErrRateLimitExceeded.Code, Message: ErrRateLimitExceeded.Message, Internal: err, } }, } /* RateLimiter returns a rate limiting middleware e := echo.New() limiterStore := middleware.NewRateLimiterMemoryStore(20) e.GET("/rate-limited", func(c echo.Context) error { return c.String(http.StatusOK, "test") }, RateLimiter(limiterStore)) */ func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc { config := DefaultRateLimiterConfig config.Store = store return RateLimiterWithConfig(config) } /* RateLimiterWithConfig returns a rate limiting middleware e := echo.New() config := middleware.RateLimiterConfig{ Skipper: DefaultSkipper, Store: middleware.NewRateLimiterMemoryStore( middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute} ) IdentifierExtractor: func(ctx echo.Context) (string, error) { id := ctx.RealIP() return id, nil }, ErrorHandler: func(context echo.Context, err error) error { return context.JSON(http.StatusTooManyRequests, nil) }, DenyHandler: func(context echo.Context, identifier string) error { return context.JSON(http.StatusForbidden, nil) }, } e.GET("/rate-limited", func(c echo.Context) error { return c.String(http.StatusOK, "test") }, middleware.RateLimiterWithConfig(config)) */ func RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc { if config.Skipper == nil { config.Skipper = DefaultRateLimiterConfig.Skipper } if config.IdentifierExtractor == nil { config.IdentifierExtractor = DefaultRateLimiterConfig.IdentifierExtractor } if config.ErrorHandler == nil { config.ErrorHandler = DefaultRateLimiterConfig.ErrorHandler } if config.DenyHandler == nil { config.DenyHandler = DefaultRateLimiterConfig.DenyHandler } if config.Store == nil { panic("Store configuration must be provided") } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } if config.BeforeFunc != nil { config.BeforeFunc(c) } identifier, err := config.IdentifierExtractor(c) if err != nil { c.Error(config.ErrorHandler(c, err)) return nil } if allow, err := config.Store.Allow(identifier); !allow { c.Error(config.DenyHandler(c, identifier, err)) return nil } return next(c) } } } type ( // RateLimiterMemoryStore is the built-in store implementation for RateLimiter RateLimiterMemoryStore struct { visitors map[string]*Visitor mutex sync.Mutex rate rate.Limit // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit. burst int expiresIn time.Duration lastCleanup time.Time timeNow func() time.Time } // Visitor signifies a unique user's limiter details Visitor struct { *rate.Limiter lastSeen time.Time } ) /* NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with the provided rate (as req/s). for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit. Burst and ExpiresIn will be set to default values. Note that if the provided rate is a float number and Burst is zero, Burst will be treated as the rounded down value of the rate. Example (with 20 requests/sec): limiterStore := middleware.NewRateLimiterMemoryStore(20) */ func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore) { return NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{ Rate: rate, }) } /* NewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore with the provided configuration. Rate must be provided. Burst will be set to the rounded down value of the configured rate if not provided or set to 0. The build-in memory store is usually capable for modest loads. For higher loads other store implementations should be considered. Characteristics: * Concurrency above 100 parallel requests may causes measurable lock contention * A high number of different IP addresses (above 16000) may be impacted by the internally used Go map * A high number of requests from a single IP address may cause lock contention Example: limiterStore := middleware.NewRateLimiterMemoryStoreWithConfig( middleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minute}, ) */ func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore) { store = &RateLimiterMemoryStore{} store.rate = config.Rate store.burst = config.Burst store.expiresIn = config.ExpiresIn if config.ExpiresIn == 0 { store.expiresIn = DefaultRateLimiterMemoryStoreConfig.ExpiresIn } if config.Burst == 0 { store.burst = int(config.Rate) } store.visitors = make(map[string]*Visitor) store.timeNow = time.Now store.lastCleanup = store.timeNow() return } // RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore type RateLimiterMemoryStoreConfig struct { Rate rate.Limit // Rate of requests allowed to pass as req/s. For more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit. Burst int // Burst is maximum number of requests to pass at the same moment. It additionally allows a number of requests to pass when rate limit is reached. ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up } // DefaultRateLimiterMemoryStoreConfig provides default configuration values for RateLimiterMemoryStore var DefaultRateLimiterMemoryStoreConfig = RateLimiterMemoryStoreConfig{ ExpiresIn: 3 * time.Minute, } // Allow implements RateLimiterStore.Allow func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) { store.mutex.Lock() limiter, exists := store.visitors[identifier] if !exists { limiter = new(Visitor) limiter.Limiter = rate.NewLimiter(store.rate, store.burst) store.visitors[identifier] = limiter } now := store.timeNow() limiter.lastSeen = now if now.Sub(store.lastCleanup) > store.expiresIn { store.cleanupStaleVisitors() } store.mutex.Unlock() return limiter.AllowN(store.timeNow(), 1), nil } /* cleanupStaleVisitors helps manage the size of the visitors map by removing stale records of users who haven't visited again after the configured expiry time has elapsed */ func (store *RateLimiterMemoryStore) cleanupStaleVisitors() { for id, visitor := range store.visitors { if store.timeNow().Sub(visitor.lastSeen) > store.expiresIn { delete(store.visitors, id) } } store.lastCleanup = store.timeNow() } golang-github-labstack-echo-4.11.1/middleware/rate_limiter_test.go000066400000000000000000000273141445647333300252310ustar00rootroot00000000000000package middleware import ( "errors" "math/rand" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/labstack/echo/v4" "github.com/labstack/gommon/random" "github.com/stretchr/testify/assert" "golang.org/x/time/rate" ) func TestRateLimiter(t *testing.T) { e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) mw := RateLimiter(inMemoryStore) testCases := []struct { id string code int }{ {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, } for _, tc := range testCases { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, tc.id) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _ = mw(handler)(c) assert.Equal(t, tc.code, rec.Code) } } func TestRateLimiter_panicBehaviour(t *testing.T) { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) assert.Panics(t, func() { RateLimiter(nil) }) assert.NotPanics(t, func() { RateLimiter(inMemoryStore) }) } func TestRateLimiterWithConfig(t *testing.T) { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } mw := RateLimiterWithConfig(RateLimiterConfig{ IdentifierExtractor: func(c echo.Context) (string, error) { id := c.Request().Header.Get(echo.HeaderXRealIP) if id == "" { return "", errors.New("invalid identifier") } return id, nil }, DenyHandler: func(ctx echo.Context, identifier string, err error) error { return ctx.JSON(http.StatusForbidden, nil) }, ErrorHandler: func(ctx echo.Context, err error) error { return ctx.JSON(http.StatusBadRequest, nil) }, Store: inMemoryStore, }) testCases := []struct { id string code int }{ {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusForbidden}, {"", http.StatusBadRequest}, {"127.0.0.1", http.StatusForbidden}, {"127.0.0.1", http.StatusForbidden}, } for _, tc := range testCases { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, tc.id) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _ = mw(handler)(c) assert.Equal(t, tc.code, rec.Code) } } func TestRateLimiterWithConfig_defaultDenyHandler(t *testing.T) { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } mw := RateLimiterWithConfig(RateLimiterConfig{ IdentifierExtractor: func(c echo.Context) (string, error) { id := c.Request().Header.Get(echo.HeaderXRealIP) if id == "" { return "", errors.New("invalid identifier") } return id, nil }, Store: inMemoryStore, }) testCases := []struct { id string code int }{ {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusTooManyRequests}, {"", http.StatusForbidden}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, } for _, tc := range testCases { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, tc.id) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _ = mw(handler)(c) assert.Equal(t, tc.code, rec.Code) } } func TestRateLimiterWithConfig_defaultConfig(t *testing.T) { { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } mw := RateLimiterWithConfig(RateLimiterConfig{ Store: inMemoryStore, }) testCases := []struct { id string code int }{ {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusOK}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, {"127.0.0.1", http.StatusTooManyRequests}, } for _, tc := range testCases { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, tc.id) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _ = mw(handler)(c) assert.Equal(t, tc.code, rec.Code) } } } func TestRateLimiterWithConfig_skipper(t *testing.T) { e := echo.New() var beforeFuncRan bool handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } var inMemoryStore = NewRateLimiterMemoryStore(5) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, "127.0.0.1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw := RateLimiterWithConfig(RateLimiterConfig{ Skipper: func(c echo.Context) bool { return true }, BeforeFunc: func(c echo.Context) { beforeFuncRan = true }, Store: inMemoryStore, IdentifierExtractor: func(ctx echo.Context) (string, error) { return "127.0.0.1", nil }, }) _ = mw(handler)(c) assert.Equal(t, false, beforeFuncRan) } func TestRateLimiterWithConfig_skipperNoSkip(t *testing.T) { e := echo.New() var beforeFuncRan bool handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } var inMemoryStore = NewRateLimiterMemoryStore(5) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, "127.0.0.1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw := RateLimiterWithConfig(RateLimiterConfig{ Skipper: func(c echo.Context) bool { return false }, BeforeFunc: func(c echo.Context) { beforeFuncRan = true }, Store: inMemoryStore, IdentifierExtractor: func(ctx echo.Context) (string, error) { return "127.0.0.1", nil }, }) _ = mw(handler)(c) assert.Equal(t, true, beforeFuncRan) } func TestRateLimiterWithConfig_beforeFunc(t *testing.T) { e := echo.New() handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } var beforeRan bool var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRealIP, "127.0.0.1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw := RateLimiterWithConfig(RateLimiterConfig{ BeforeFunc: func(c echo.Context) { beforeRan = true }, Store: inMemoryStore, IdentifierExtractor: func(ctx echo.Context) (string, error) { return "127.0.0.1", nil }, }) _ = mw(handler)(c) assert.Equal(t, true, beforeRan) } func TestRateLimiterMemoryStore_Allow(t *testing.T) { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3, ExpiresIn: 2 * time.Second}) testCases := []struct { id string allowed bool }{ {"127.0.0.1", true}, // 0 ms {"127.0.0.1", true}, // 220 ms burst #2 {"127.0.0.1", true}, // 440 ms burst #3 {"127.0.0.1", false}, // 660 ms block {"127.0.0.1", false}, // 880 ms block {"127.0.0.1", true}, // 1100 ms next second #1 {"127.0.0.2", true}, // 1320 ms allow other ip {"127.0.0.1", false}, // 1540 ms no burst {"127.0.0.1", false}, // 1760 ms no burst {"127.0.0.1", false}, // 1980 ms no burst {"127.0.0.1", true}, // 2200 ms no burst {"127.0.0.1", false}, // 2420 ms no burst {"127.0.0.1", false}, // 2640 ms no burst {"127.0.0.1", false}, // 2860 ms no burst {"127.0.0.1", true}, // 3080 ms no burst {"127.0.0.1", false}, // 3300 ms no burst {"127.0.0.1", false}, // 3520 ms no burst {"127.0.0.1", false}, // 3740 ms no burst {"127.0.0.1", false}, // 3960 ms no burst {"127.0.0.1", true}, // 4180 ms no burst {"127.0.0.1", false}, // 4400 ms no burst {"127.0.0.1", false}, // 4620 ms no burst {"127.0.0.1", false}, // 4840 ms no burst {"127.0.0.1", true}, // 5060 ms no burst } for i, tc := range testCases { t.Logf("Running testcase #%d => %v", i, time.Duration(i)*220*time.Millisecond) inMemoryStore.timeNow = func() time.Time { return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Add(time.Duration(i) * 220 * time.Millisecond) } allowed, _ := inMemoryStore.Allow(tc.id) assert.Equal(t, tc.allowed, allowed) } } func TestRateLimiterMemoryStore_cleanupStaleVisitors(t *testing.T) { var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3}) inMemoryStore.visitors = map[string]*Visitor{ "A": { Limiter: rate.NewLimiter(1, 3), lastSeen: time.Now(), }, "B": { Limiter: rate.NewLimiter(1, 3), lastSeen: time.Now().Add(-1 * time.Minute), }, "C": { Limiter: rate.NewLimiter(1, 3), lastSeen: time.Now().Add(-5 * time.Minute), }, "D": { Limiter: rate.NewLimiter(1, 3), lastSeen: time.Now().Add(-10 * time.Minute), }, } inMemoryStore.Allow("D") inMemoryStore.cleanupStaleVisitors() var exists bool _, exists = inMemoryStore.visitors["A"] assert.Equal(t, true, exists) _, exists = inMemoryStore.visitors["B"] assert.Equal(t, true, exists) _, exists = inMemoryStore.visitors["C"] assert.Equal(t, false, exists) _, exists = inMemoryStore.visitors["D"] assert.Equal(t, true, exists) } func TestNewRateLimiterMemoryStore(t *testing.T) { testCases := []struct { rate rate.Limit burst int expiresIn time.Duration expectedExpiresIn time.Duration }{ {1, 3, 5 * time.Second, 5 * time.Second}, {2, 4, 0, 3 * time.Minute}, {1, 5, 10 * time.Minute, 10 * time.Minute}, {3, 7, 0, 3 * time.Minute}, } for _, tc := range testCases { store := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: tc.rate, Burst: tc.burst, ExpiresIn: tc.expiresIn}) assert.Equal(t, tc.rate, store.rate) assert.Equal(t, tc.burst, store.burst) assert.Equal(t, tc.expectedExpiresIn, store.expiresIn) } } func generateAddressList(count int) []string { addrs := make([]string, count) for i := 0; i < count; i++ { addrs[i] = random.String(15) } return addrs } func run(wg *sync.WaitGroup, store RateLimiterStore, addrs []string, max int, b *testing.B) { for i := 0; i < b.N; i++ { store.Allow(addrs[rand.Intn(max)]) } wg.Done() } func benchmarkStore(store RateLimiterStore, parallel int, max int, b *testing.B) { addrs := generateAddressList(max) wg := &sync.WaitGroup{} for i := 0; i < parallel; i++ { wg.Add(1) go run(wg, store, addrs, max, b) } wg.Wait() } const ( testExpiresIn = 1000 * time.Millisecond ) func BenchmarkRateLimiterMemoryStore_1000(b *testing.B) { var store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn}) benchmarkStore(store, 10, 1000, b) } func BenchmarkRateLimiterMemoryStore_10000(b *testing.B) { var store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn}) benchmarkStore(store, 10, 10000, b) } func BenchmarkRateLimiterMemoryStore_100000(b *testing.B) { var store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn}) benchmarkStore(store, 10, 100000, b) } func BenchmarkRateLimiterMemoryStore_conc100_10000(b *testing.B) { var store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn}) benchmarkStore(store, 100, 10000, b) } golang-github-labstack-echo-4.11.1/middleware/recover.go000066400000000000000000000071751445647333300231620ustar00rootroot00000000000000package middleware import ( "fmt" "net/http" "runtime" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" ) type ( // LogErrorFunc defines a function for custom logging in the middleware. LogErrorFunc func(c echo.Context, err error, stack []byte) error // RecoverConfig defines the config for Recover middleware. RecoverConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Size of the stack to be printed. // Optional. Default value 4KB. StackSize int `yaml:"stack_size"` // DisableStackAll disables formatting stack traces of all other goroutines // into buffer after the trace for the current goroutine. // Optional. Default value false. DisableStackAll bool `yaml:"disable_stack_all"` // DisablePrintStack disables printing stack trace. // Optional. Default value as false. DisablePrintStack bool `yaml:"disable_print_stack"` // LogLevel is log level to printing stack trace. // Optional. Default value 0 (Print). LogLevel log.Lvl // LogErrorFunc defines a function for custom logging in the middleware. // If it's set you don't need to provide LogLevel for config. // If this function returns nil, the centralized HTTPErrorHandler will not be called. LogErrorFunc LogErrorFunc // DisableErrorHandler disables the call to centralized HTTPErrorHandler. // The recovered error is then passed back to upstream middleware, instead of swallowing the error. // Optional. Default value false. DisableErrorHandler bool `yaml:"disable_error_handler"` } ) var ( // DefaultRecoverConfig is the default Recover middleware config. DefaultRecoverConfig = RecoverConfig{ Skipper: DefaultSkipper, StackSize: 4 << 10, // 4 KB DisableStackAll: false, DisablePrintStack: false, LogLevel: 0, LogErrorFunc: nil, DisableErrorHandler: false, } ) // Recover returns a middleware which recovers from panics anywhere in the chain // and handles the control to the centralized HTTPErrorHandler. func Recover() echo.MiddlewareFunc { return RecoverWithConfig(DefaultRecoverConfig) } // RecoverWithConfig returns a Recover middleware with config. // See: `Recover()`. func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultRecoverConfig.Skipper } if config.StackSize == 0 { config.StackSize = DefaultRecoverConfig.StackSize } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (returnErr error) { if config.Skipper(c) { return next(c) } defer func() { if r := recover(); r != nil { if r == http.ErrAbortHandler { panic(r) } err, ok := r.(error) if !ok { err = fmt.Errorf("%v", r) } var stack []byte var length int if !config.DisablePrintStack { stack = make([]byte, config.StackSize) length = runtime.Stack(stack, !config.DisableStackAll) stack = stack[:length] } if config.LogErrorFunc != nil { err = config.LogErrorFunc(c, err, stack) } else if !config.DisablePrintStack { msg := fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack[:length]) switch config.LogLevel { case log.DEBUG: c.Logger().Debug(msg) case log.INFO: c.Logger().Info(msg) case log.WARN: c.Logger().Warn(msg) case log.ERROR: c.Logger().Error(msg) case log.OFF: // None. default: c.Logger().Print(msg) } } if err != nil && !config.DisableErrorHandler { c.Error(err) } else { returnErr = err } } }() return next(c) } } } golang-github-labstack-echo-4.11.1/middleware/recover_test.go000066400000000000000000000105161445647333300242120ustar00rootroot00000000000000package middleware import ( "bytes" "errors" "fmt" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" "github.com/stretchr/testify/assert" ) func TestRecover(t *testing.T) { e := echo.New() buf := new(bytes.Buffer) e.Logger.SetOutput(buf) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Recover()(echo.HandlerFunc(func(c echo.Context) error { panic("test") })) err := h(c) assert.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, buf.String(), "PANIC RECOVER") } func TestRecoverErrAbortHandler(t *testing.T) { e := echo.New() buf := new(bytes.Buffer) e.Logger.SetOutput(buf) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := Recover()(echo.HandlerFunc(func(c echo.Context) error { panic(http.ErrAbortHandler) })) defer func() { r := recover() if r == nil { assert.Fail(t, "expecting `http.ErrAbortHandler`, got `nil`") } else { if err, ok := r.(error); ok { assert.ErrorIs(t, err, http.ErrAbortHandler) } else { assert.Fail(t, "not of error type") } } }() h(c) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.NotContains(t, buf.String(), "PANIC RECOVER") } func TestRecoverWithConfig_LogLevel(t *testing.T) { tests := []struct { logLevel log.Lvl levelName string }{{ logLevel: log.DEBUG, levelName: "DEBUG", }, { logLevel: log.INFO, levelName: "INFO", }, { logLevel: log.WARN, levelName: "WARN", }, { logLevel: log.ERROR, levelName: "ERROR", }, { logLevel: log.OFF, levelName: "OFF", }} for _, tt := range tests { tt := tt t.Run(tt.levelName, func(t *testing.T) { e := echo.New() e.Logger.SetLevel(log.DEBUG) buf := new(bytes.Buffer) e.Logger.SetOutput(buf) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) config := DefaultRecoverConfig config.LogLevel = tt.logLevel h := RecoverWithConfig(config)(echo.HandlerFunc(func(c echo.Context) error { panic("test") })) h(c) assert.Equal(t, http.StatusInternalServerError, rec.Code) output := buf.String() if tt.logLevel == log.OFF { assert.Empty(t, output) } else { assert.Contains(t, output, "PANIC RECOVER") assert.Contains(t, output, fmt.Sprintf(`"level":"%s"`, tt.levelName)) } }) } } func TestRecoverWithConfig_LogErrorFunc(t *testing.T) { e := echo.New() e.Logger.SetLevel(log.DEBUG) buf := new(bytes.Buffer) e.Logger.SetOutput(buf) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) testError := errors.New("test") config := DefaultRecoverConfig config.LogErrorFunc = func(c echo.Context, err error, stack []byte) error { msg := fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack) if errors.Is(err, testError) { c.Logger().Debug(msg) } else { c.Logger().Error(msg) } return err } t.Run("first branch case for LogErrorFunc", func(t *testing.T) { buf.Reset() h := RecoverWithConfig(config)(echo.HandlerFunc(func(c echo.Context) error { panic(testError) })) h(c) assert.Equal(t, http.StatusInternalServerError, rec.Code) output := buf.String() assert.Contains(t, output, "PANIC RECOVER") assert.Contains(t, output, `"level":"DEBUG"`) }) t.Run("else branch case for LogErrorFunc", func(t *testing.T) { buf.Reset() h := RecoverWithConfig(config)(echo.HandlerFunc(func(c echo.Context) error { panic("other") })) h(c) assert.Equal(t, http.StatusInternalServerError, rec.Code) output := buf.String() assert.Contains(t, output, "PANIC RECOVER") assert.Contains(t, output, `"level":"ERROR"`) }) } func TestRecoverWithDisabled_ErrorHandler(t *testing.T) { e := echo.New() buf := new(bytes.Buffer) e.Logger.SetOutput(buf) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) config := DefaultRecoverConfig config.DisableErrorHandler = true h := RecoverWithConfig(config)(echo.HandlerFunc(func(c echo.Context) error { panic("test") })) err := h(c) assert.Equal(t, http.StatusOK, rec.Code) assert.Contains(t, buf.String(), "PANIC RECOVER") assert.EqualError(t, err, "test") } golang-github-labstack-echo-4.11.1/middleware/redirect.go000066400000000000000000000111161445647333300233040ustar00rootroot00000000000000package middleware import ( "net/http" "strings" "github.com/labstack/echo/v4" ) // RedirectConfig defines the config for Redirect middleware. type RedirectConfig struct { // Skipper defines a function to skip middleware. Skipper // Status code to be used when redirecting the request. // Optional. Default value http.StatusMovedPermanently. Code int `yaml:"code"` } // redirectLogic represents a function that given a scheme, host and uri // can both: 1) determine if redirect is needed (will set ok accordingly) and // 2) return the appropriate redirect url. type redirectLogic func(scheme, host, uri string) (ok bool, url string) const www = "www." // DefaultRedirectConfig is the default Redirect middleware config. var DefaultRedirectConfig = RedirectConfig{ Skipper: DefaultSkipper, Code: http.StatusMovedPermanently, } // HTTPSRedirect redirects http requests to https. // For example, http://labstack.com will be redirect to https://labstack.com. // // Usage `Echo#Pre(HTTPSRedirect())` func HTTPSRedirect() echo.MiddlewareFunc { return HTTPSRedirectWithConfig(DefaultRedirectConfig) } // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. // See `HTTPSRedirect()`. func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { return redirect(config, func(scheme, host, uri string) (bool, string) { if scheme != "https" { return true, "https://" + host + uri } return false, "" }) } // HTTPSWWWRedirect redirects http requests to https www. // For example, http://labstack.com will be redirect to https://www.labstack.com. // // Usage `Echo#Pre(HTTPSWWWRedirect())` func HTTPSWWWRedirect() echo.MiddlewareFunc { return HTTPSWWWRedirectWithConfig(DefaultRedirectConfig) } // HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // See `HTTPSWWWRedirect()`. func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { return redirect(config, func(scheme, host, uri string) (bool, string) { if scheme != "https" && !strings.HasPrefix(host, www) { return true, "https://www." + host + uri } return false, "" }) } // HTTPSNonWWWRedirect redirects http requests to https non www. // For example, http://www.labstack.com will be redirect to https://labstack.com. // // Usage `Echo#Pre(HTTPSNonWWWRedirect())` func HTTPSNonWWWRedirect() echo.MiddlewareFunc { return HTTPSNonWWWRedirectWithConfig(DefaultRedirectConfig) } // HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // See `HTTPSNonWWWRedirect()`. func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { return redirect(config, func(scheme, host, uri string) (ok bool, url string) { if scheme != "https" { host = strings.TrimPrefix(host, www) return true, "https://" + host + uri } return false, "" }) } // WWWRedirect redirects non www requests to www. // For example, http://labstack.com will be redirect to http://www.labstack.com. // // Usage `Echo#Pre(WWWRedirect())` func WWWRedirect() echo.MiddlewareFunc { return WWWRedirectWithConfig(DefaultRedirectConfig) } // WWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // See `WWWRedirect()`. func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { return redirect(config, func(scheme, host, uri string) (bool, string) { if !strings.HasPrefix(host, www) { return true, scheme + "://www." + host + uri } return false, "" }) } // NonWWWRedirect redirects www requests to non www. // For example, http://www.labstack.com will be redirect to http://labstack.com. // // Usage `Echo#Pre(NonWWWRedirect())` func NonWWWRedirect() echo.MiddlewareFunc { return NonWWWRedirectWithConfig(DefaultRedirectConfig) } // NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // See `NonWWWRedirect()`. func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { return redirect(config, func(scheme, host, uri string) (bool, string) { if strings.HasPrefix(host, www) { return true, scheme + "://" + host[4:] + uri } return false, "" }) } func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc { if config.Skipper == nil { config.Skipper = DefaultRedirectConfig.Skipper } if config.Code == 0 { config.Code = DefaultRedirectConfig.Code } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req, scheme := c.Request(), c.Scheme() host := req.Host if ok, url := cb(scheme, host, req.RequestURI); ok { return c.Redirect(config.Code, url) } return next(c) } } } golang-github-labstack-echo-4.11.1/middleware/redirect_test.go000066400000000000000000000161421445647333300243470ustar00rootroot00000000000000package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) type middlewareGenerator func() echo.MiddlewareFunc func TestRedirectHTTPSRedirect(t *testing.T) { var testCases = []struct { whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { whenHost: "labstack.com", expectLocation: "https://labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "labstack.com", whenHeader: map[string][]string{echo.HeaderXForwardedProto: {"https"}}, expectLocation: "", expectStatusCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { res := redirectTest(HTTPSRedirect, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func TestRedirectHTTPSWWWRedirect(t *testing.T) { var testCases = []struct { whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { whenHost: "labstack.com", expectLocation: "https://www.labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "www.labstack.com", expectLocation: "", expectStatusCode: http.StatusOK, }, { whenHost: "a.com", expectLocation: "https://www.a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "ip", expectLocation: "https://www.ip/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "labstack.com", whenHeader: map[string][]string{echo.HeaderXForwardedProto: {"https"}}, expectLocation: "", expectStatusCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { res := redirectTest(HTTPSWWWRedirect, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func TestRedirectHTTPSNonWWWRedirect(t *testing.T) { var testCases = []struct { whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { whenHost: "www.labstack.com", expectLocation: "https://labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "a.com", expectLocation: "https://a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "ip", expectLocation: "https://ip/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "www.labstack.com", whenHeader: map[string][]string{echo.HeaderXForwardedProto: {"https"}}, expectLocation: "", expectStatusCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { res := redirectTest(HTTPSNonWWWRedirect, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func TestRedirectWWWRedirect(t *testing.T) { var testCases = []struct { whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { whenHost: "labstack.com", expectLocation: "http://www.labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "a.com", expectLocation: "http://www.a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "ip", expectLocation: "http://www.ip/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "a.com", whenHeader: map[string][]string{echo.HeaderXForwardedProto: {"https"}}, expectLocation: "https://www.a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "www.ip", expectLocation: "", expectStatusCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { res := redirectTest(WWWRedirect, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func TestRedirectNonWWWRedirect(t *testing.T) { var testCases = []struct { whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { whenHost: "www.labstack.com", expectLocation: "http://labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "www.a.com", expectLocation: "http://a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "www.a.com", whenHeader: map[string][]string{echo.HeaderXForwardedProto: {"https"}}, expectLocation: "https://a.com/", expectStatusCode: http.StatusMovedPermanently, }, { whenHost: "ip", expectLocation: "", expectStatusCode: http.StatusOK, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { res := redirectTest(NonWWWRedirect, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func TestNonWWWRedirectWithConfig(t *testing.T) { var testCases = []struct { name string givenCode int givenSkipFunc func(c echo.Context) bool whenHost string whenHeader http.Header expectLocation string expectStatusCode int }{ { name: "usual redirect", whenHost: "www.labstack.com", expectLocation: "http://labstack.com/", expectStatusCode: http.StatusMovedPermanently, }, { name: "redirect is skipped", givenSkipFunc: func(c echo.Context) bool { return true // skip always }, whenHost: "www.labstack.com", expectLocation: "", expectStatusCode: http.StatusOK, }, { name: "redirect with custom status code", givenCode: http.StatusSeeOther, whenHost: "www.labstack.com", expectLocation: "http://labstack.com/", expectStatusCode: http.StatusSeeOther, }, } for _, tc := range testCases { t.Run(tc.whenHost, func(t *testing.T) { middleware := func() echo.MiddlewareFunc { return NonWWWRedirectWithConfig(RedirectConfig{ Skipper: tc.givenSkipFunc, Code: tc.givenCode, }) } res := redirectTest(middleware, tc.whenHost, tc.whenHeader) assert.Equal(t, tc.expectStatusCode, res.Code) assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation)) }) } } func redirectTest(fn middlewareGenerator, host string, header http.Header) *httptest.ResponseRecorder { e := echo.New() next := func(c echo.Context) (err error) { return c.NoContent(http.StatusOK) } req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = host if header != nil { req.Header = header } res := httptest.NewRecorder() c := e.NewContext(req, res) fn()(next)(c) return res } golang-github-labstack-echo-4.11.1/middleware/request_id.go000066400000000000000000000035051445647333300236520ustar00rootroot00000000000000package middleware import ( "github.com/labstack/echo/v4" "github.com/labstack/gommon/random" ) type ( // RequestIDConfig defines the config for RequestID middleware. RequestIDConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Generator defines a function to generate an ID. // Optional. Default value random.String(32). Generator func() string // RequestIDHandler defines a function which is executed for a request id. RequestIDHandler func(echo.Context, string) // TargetHeader defines what header to look for to populate the id TargetHeader string } ) var ( // DefaultRequestIDConfig is the default RequestID middleware config. DefaultRequestIDConfig = RequestIDConfig{ Skipper: DefaultSkipper, Generator: generator, TargetHeader: echo.HeaderXRequestID, } ) // RequestID returns a X-Request-ID middleware. func RequestID() echo.MiddlewareFunc { return RequestIDWithConfig(DefaultRequestIDConfig) } // RequestIDWithConfig returns a X-Request-ID middleware with config. func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultRequestIDConfig.Skipper } if config.Generator == nil { config.Generator = generator } if config.TargetHeader == "" { config.TargetHeader = echo.HeaderXRequestID } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() res := c.Response() rid := req.Header.Get(config.TargetHeader) if rid == "" { rid = config.Generator() } res.Header().Set(config.TargetHeader, rid) if config.RequestIDHandler != nil { config.RequestIDHandler(c, rid) } return next(c) } } } func generator() string { return random.String(32) } golang-github-labstack-echo-4.11.1/middleware/request_id_test.go000066400000000000000000000045231445647333300247120ustar00rootroot00000000000000package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestRequestID(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } rid := RequestIDWithConfig(RequestIDConfig{}) h := rid(handler) h(c) assert.Len(t, rec.Header().Get(echo.HeaderXRequestID), 32) // Custom generator and handler customID := "customGenerator" calledHandler := false rid = RequestIDWithConfig(RequestIDConfig{ Generator: func() string { return customID }, RequestIDHandler: func(_ echo.Context, id string) { calledHandler = true assert.Equal(t, customID, id) }, }) h = rid(handler) h(c) assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator") assert.True(t, calledHandler) } func TestRequestID_IDNotAltered(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderXRequestID, "") rec := httptest.NewRecorder() c := e.NewContext(req, rec) handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } rid := RequestIDWithConfig(RequestIDConfig{}) h := rid(handler) _ = h(c) assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "") } func TestRequestIDConfigDifferentHeader(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) handler := func(c echo.Context) error { return c.String(http.StatusOK, "test") } rid := RequestIDWithConfig(RequestIDConfig{TargetHeader: echo.HeaderXCorrelationID}) h := rid(handler) h(c) assert.Len(t, rec.Header().Get(echo.HeaderXCorrelationID), 32) // Custom generator and handler customID := "customGenerator" calledHandler := false rid = RequestIDWithConfig(RequestIDConfig{ Generator: func() string { return customID }, TargetHeader: echo.HeaderXCorrelationID, RequestIDHandler: func(_ echo.Context, id string) { calledHandler = true assert.Equal(t, customID, id) }, }) h = rid(handler) h(c) assert.Equal(t, rec.Header().Get(echo.HeaderXCorrelationID), "customGenerator") assert.True(t, calledHandler) } golang-github-labstack-echo-4.11.1/middleware/request_logger.go000066400000000000000000000315511445647333300245370ustar00rootroot00000000000000package middleware import ( "errors" "net/http" "time" "github.com/labstack/echo/v4" ) // Example for `fmt.Printf` // e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ // LogStatus: true, // LogURI: true, // LogError: true, // HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code // LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { // if v.Error == nil { // fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status) // } else { // fmt.Printf("REQUEST_ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error) // } // return nil // }, // })) // // Example for Zerolog (https://github.com/rs/zerolog) // logger := zerolog.New(os.Stdout) // e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ // LogURI: true, // LogStatus: true, // LogError: true, // HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code // LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { // if v.Error == nil { // logger.Info(). // Str("URI", v.URI). // Int("status", v.Status). // Msg("request") // } else { // logger.Error(). // Err(v.Error). // Str("URI", v.URI). // Int("status", v.Status). // Msg("request error") // } // return nil // }, // })) // // Example for Zap (https://github.com/uber-go/zap) // logger, _ := zap.NewProduction() // e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ // LogURI: true, // LogStatus: true, // LogError: true, // HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code // LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { // if v.Error == nil { // logger.Info("request", // zap.String("URI", v.URI), // zap.Int("status", v.Status), // ) // } else { // logger.Error("request error", // zap.String("URI", v.URI), // zap.Int("status", v.Status), // zap.Error(v.Error), // ) // } // return nil // }, // })) // // Example for Logrus (https://github.com/sirupsen/logrus) // log := logrus.New() // e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ // LogURI: true, // LogStatus: true, // LogError: true, // HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code // LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { // if v.Error == nil { // log.WithFields(logrus.Fields{ // "URI": v.URI, // "status": v.Status, // }).Info("request") // } else { // log.WithFields(logrus.Fields{ // "URI": v.URI, // "status": v.Status, // "error": v.Error, // }).Error("request error") // } // return nil // }, // })) // RequestLoggerConfig is configuration for Request Logger middleware. type RequestLoggerConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // BeforeNextFunc defines a function that is called before next middleware or handler is called in chain. BeforeNextFunc func(c echo.Context) // LogValuesFunc defines a function that is called with values extracted by logger from request/response. // Mandatory. LogValuesFunc func(c echo.Context, v RequestLoggerValues) error // HandleError instructs logger to call global error handler when next middleware/handler returns an error. // This is useful when you have custom error handler that can decide to use different status codes. // // A side-effect of calling global error handler is that now Response has been committed and sent to the client // and middlewares up in chain can not change Response status code or response body. HandleError bool // LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call). LogLatency bool // LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`) LogProtocol bool // LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details. LogRemoteIP bool // LogHost instructs logger to extract request host value (i.e. `example.com`) LogHost bool // LogMethod instructs logger to extract request method value (i.e. `GET` etc) LogMethod bool // LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`) LogURI bool // LogURIPath instructs logger to extract request URI path part (i.e. `/list`) LogURIPath bool // LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`) LogRoutePath bool // LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value. LogRequestID bool // LogReferer instructs logger to extract request referer values. LogReferer bool // LogUserAgent instructs logger to extract request user agent values. LogUserAgent bool // LogStatus instructs logger to extract response status code. If handler chain returns an echo.HTTPError, // the status code is extracted from the echo.HTTPError returned LogStatus bool // LogError instructs logger to extract error returned from executed handler chain. LogError bool // LogContentLength instructs logger to extract content length header value. Note: this value could be different from // actual request body size as it could be spoofed etc. LogContentLength bool // LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware // this value may not be always correct. LogResponseSize bool // LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than // one header with same value so slice of values is been logger for each given header. // // Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header // names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding". LogHeaders []string // LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can // contain more than one query parameter with same name so slice of values is been logger for each given query param name. LogQueryParams []string // LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can // contain more than one form value with same name so slice of values is been logger for each given form value name. LogFormValues []string timeNow func() time.Time } // RequestLoggerValues contains extracted values from logger. type RequestLoggerValues struct { // StartTime is time recorded before next middleware/handler is executed. StartTime time.Time // Latency is duration it took to execute rest of the handler chain (next(c) call). Latency time.Duration // Protocol is request protocol (i.e. `HTTP/1.1` or `HTTP/2`) Protocol string // RemoteIP is request remote IP. See `echo.Context.RealIP()` for implementation details. RemoteIP string // Host is request host value (i.e. `example.com`) Host string // Method is request method value (i.e. `GET` etc) Method string // URI is request URI (i.e. `/list?lang=en&page=1`) URI string // URIPath is request URI path part (i.e. `/list`) URIPath string // RoutePath is route path part to which request was matched to (i.e. `/user/:id`) RoutePath string // RequestID is request ID from request `X-Request-ID` header or response if request did not have value. RequestID string // Referer is request referer values. Referer string // UserAgent is request user agent values. UserAgent string // Status is response status code. Then handler returns an echo.HTTPError then code from there. Status int // Error is error returned from executed handler chain. Error error // ContentLength is content length header value. Note: this value could be different from actual request body size // as it could be spoofed etc. ContentLength string // ResponseSize is response content length value. Note: when used with Gzip middleware this value may not be always correct. ResponseSize int64 // Headers are list of headers from request. Note: request can contain more than one header with same value so slice // of values is been logger for each given header. // Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header // names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding". Headers map[string][]string // QueryParams are list of query parameters from request URI. Note: request can contain more than one query parameter // with same name so slice of values is been logger for each given query param name. QueryParams map[string][]string // FormValues are list of form values from request body+URI. Note: request can contain more than one form value with // same name so slice of values is been logger for each given form value name. FormValues map[string][]string } // RequestLoggerWithConfig returns a RequestLogger middleware with config. func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc { mw, err := config.ToMiddleware() if err != nil { panic(err) } return mw } // ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration. func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if config.Skipper == nil { config.Skipper = DefaultSkipper } now := time.Now if config.timeNow != nil { now = config.timeNow } if config.LogValuesFunc == nil { return nil, errors.New("missing LogValuesFunc callback function for request logger middleware") } logHeaders := len(config.LogHeaders) > 0 headers := append([]string(nil), config.LogHeaders...) for i, v := range headers { headers[i] = http.CanonicalHeaderKey(v) } logQueryParams := len(config.LogQueryParams) > 0 logFormValues := len(config.LogFormValues) > 0 return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() res := c.Response() start := now() if config.BeforeNextFunc != nil { config.BeforeNextFunc(c) } err := next(c) if err != nil && config.HandleError { c.Error(err) } v := RequestLoggerValues{ StartTime: start, } if config.LogLatency { v.Latency = now().Sub(start) } if config.LogProtocol { v.Protocol = req.Proto } if config.LogRemoteIP { v.RemoteIP = c.RealIP() } if config.LogHost { v.Host = req.Host } if config.LogMethod { v.Method = req.Method } if config.LogURI { v.URI = req.RequestURI } if config.LogURIPath { p := req.URL.Path if p == "" { p = "/" } v.URIPath = p } if config.LogRoutePath { v.RoutePath = c.Path() } if config.LogRequestID { id := req.Header.Get(echo.HeaderXRequestID) if id == "" { id = res.Header().Get(echo.HeaderXRequestID) } v.RequestID = id } if config.LogReferer { v.Referer = req.Referer() } if config.LogUserAgent { v.UserAgent = req.UserAgent() } if config.LogStatus { v.Status = res.Status if err != nil && !config.HandleError { // this block should not be executed in case of HandleError=true as the global error handler will decide // the status code. In that case status code could be different from what err contains. var httpErr *echo.HTTPError if errors.As(err, &httpErr) { v.Status = httpErr.Code } } } if config.LogError && err != nil { v.Error = err } if config.LogContentLength { v.ContentLength = req.Header.Get(echo.HeaderContentLength) } if config.LogResponseSize { v.ResponseSize = res.Size } if logHeaders { v.Headers = map[string][]string{} for _, header := range headers { if values, ok := req.Header[header]; ok { v.Headers[header] = values } } } if logQueryParams { queryParams := c.QueryParams() v.QueryParams = map[string][]string{} for _, param := range config.LogQueryParams { if values, ok := queryParams[param]; ok { v.QueryParams[param] = values } } } if logFormValues { v.FormValues = map[string][]string{} for _, formValue := range config.LogFormValues { if values, ok := req.Form[formValue]; ok { v.FormValues[formValue] = values } } } if errOnLog := config.LogValuesFunc(c, v); errOnLog != nil { return errOnLog } // in case of HandleError=true we are returning the error that we already have handled with global error handler // this is deliberate as this error could be useful for upstream middlewares and default global error handler // will ignore that error when it bubbles up in middleware chain. return err } }, nil } golang-github-labstack-echo-4.11.1/middleware/request_logger_test.go000066400000000000000000000306661445647333300256040ustar00rootroot00000000000000package middleware import ( "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "time" ) func TestRequestLoggerWithConfig(t *testing.T) { e := echo.New() var expect RequestLoggerValues e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ LogRoutePath: true, LogURI: true, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { expect = values return nil }, })) e.GET("/test", func(c echo.Context) error { return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusTeapot, rec.Code) assert.Equal(t, "/test", expect.RoutePath) } func TestRequestLoggerWithConfig_missingOnLogValuesPanics(t *testing.T) { assert.Panics(t, func() { RequestLoggerWithConfig(RequestLoggerConfig{ LogValuesFunc: nil, }) }) } func TestRequestLogger_skipper(t *testing.T) { e := echo.New() loggerCalled := false e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ Skipper: func(c echo.Context) bool { return true }, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { loggerCalled = true return nil }, })) e.GET("/test", func(c echo.Context) error { return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusTeapot, rec.Code) assert.False(t, loggerCalled) } func TestRequestLogger_beforeNextFunc(t *testing.T) { e := echo.New() var myLoggerInstance int e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ BeforeNextFunc: func(c echo.Context) { c.Set("myLoggerInstance", 42) }, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { myLoggerInstance = c.Get("myLoggerInstance").(int) return nil }, })) e.GET("/test", func(c echo.Context) error { return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusTeapot, rec.Code) assert.Equal(t, 42, myLoggerInstance) } func TestRequestLogger_logError(t *testing.T) { e := echo.New() var actual RequestLoggerValues e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ LogError: true, LogStatus: true, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { actual = values return nil }, })) e.GET("/test", func(c echo.Context) error { return echo.NewHTTPError(http.StatusNotAcceptable, "nope") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotAcceptable, rec.Code) assert.Equal(t, http.StatusNotAcceptable, actual.Status) assert.EqualError(t, actual.Error, "code=406, message=nope") } func TestRequestLogger_HandleError(t *testing.T) { e := echo.New() var actual RequestLoggerValues e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ timeNow: func() time.Time { return time.Unix(1631045377, 0).UTC() }, HandleError: true, LogError: true, LogStatus: true, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { actual = values return nil }, })) // to see if "HandleError" works we create custom error handler that uses its own status codes e.HTTPErrorHandler = func(err error, c echo.Context) { if c.Response().Committed { return } c.JSON(http.StatusTeapot, "custom error handler") } e.GET("/test", func(c echo.Context) error { return echo.NewHTTPError(http.StatusForbidden, "nope") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusTeapot, rec.Code) expect := RequestLoggerValues{ StartTime: time.Unix(1631045377, 0).UTC(), Status: http.StatusTeapot, Error: echo.NewHTTPError(http.StatusForbidden, "nope"), } assert.Equal(t, expect, actual) } func TestRequestLogger_LogValuesFuncError(t *testing.T) { e := echo.New() var expect RequestLoggerValues e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ LogError: true, LogStatus: true, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { expect = values return echo.NewHTTPError(http.StatusNotAcceptable, "LogValuesFuncError") }, })) e.GET("/test", func(c echo.Context) error { return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) // NOTE: when global error handler received error returned from middleware the status has already // been written to the client and response has been "commited" therefore global error handler does not do anything // and error that bubbled up in middleware chain will not be reflected in response code. assert.Equal(t, http.StatusTeapot, rec.Code) assert.Equal(t, http.StatusTeapot, expect.Status) } func TestRequestLogger_ID(t *testing.T) { var testCases = []struct { name string whenFromRequest bool expect string }{ { name: "ok, ID is provided from request headers", whenFromRequest: true, expect: "123", }, { name: "ok, ID is from response headers", whenFromRequest: false, expect: "321", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() var expect RequestLoggerValues e.Use(RequestLoggerWithConfig(RequestLoggerConfig{ LogRequestID: true, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { expect = values return nil }, })) e.GET("/test", func(c echo.Context) error { c.Response().Header().Set(echo.HeaderXRequestID, "321") return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test", nil) if tc.whenFromRequest { req.Header.Set(echo.HeaderXRequestID, "123") } rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusTeapot, rec.Code) assert.Equal(t, tc.expect, expect.RequestID) }) } } func TestRequestLogger_headerIsCaseInsensitive(t *testing.T) { e := echo.New() var expect RequestLoggerValues mw := RequestLoggerWithConfig(RequestLoggerConfig{ LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { expect = values return nil }, LogHeaders: []string{"referer", "User-Agent"}, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") c.FormValue("to force parse form") return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test?lang=en&checked=1&checked=2", nil) req.Header.Set("referer", "https://echo.labstack.com/") rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := mw(c) assert.NoError(t, err) assert.Len(t, expect.Headers, 1) assert.Equal(t, []string{"https://echo.labstack.com/"}, expect.Headers["Referer"]) } func TestRequestLogger_allFields(t *testing.T) { e := echo.New() isFirstNowCall := true var expect RequestLoggerValues mw := RequestLoggerWithConfig(RequestLoggerConfig{ LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { expect = values return nil }, LogLatency: true, LogProtocol: true, LogRemoteIP: true, LogHost: true, LogMethod: true, LogURI: true, LogURIPath: true, LogRoutePath: true, LogRequestID: true, LogReferer: true, LogUserAgent: true, LogStatus: true, LogError: true, LogContentLength: true, LogResponseSize: true, LogHeaders: []string{"accept-encoding", "User-Agent"}, LogQueryParams: []string{"lang", "checked"}, LogFormValues: []string{"csrf", "multiple"}, timeNow: func() time.Time { if isFirstNowCall { isFirstNowCall = false return time.Unix(1631045377, 0) } return time.Unix(1631045377+10, 0) }, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") c.FormValue("to force parse form") return c.String(http.StatusTeapot, "OK") }) f := make(url.Values) f.Set("csrf", "token") f.Set("multiple", "1") f.Add("multiple", "2") reader := strings.NewReader(f.Encode()) req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", reader) req.Header.Set("Referer", "https://echo.labstack.com/") req.Header.Set("User-Agent", "curl/7.68.0") req.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size()))) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) req.Header.Set(echo.HeaderXRealIP, "8.8.8.8") rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetPath("/test*") err := mw(c) assert.NoError(t, err) assert.Equal(t, time.Unix(1631045377, 0), expect.StartTime) assert.Equal(t, 10*time.Second, expect.Latency) assert.Equal(t, "HTTP/1.1", expect.Protocol) assert.Equal(t, "8.8.8.8", expect.RemoteIP) assert.Equal(t, "example.com", expect.Host) assert.Equal(t, http.MethodPost, expect.Method) assert.Equal(t, "/test?lang=en&checked=1&checked=2", expect.URI) assert.Equal(t, "/test", expect.URIPath) assert.Equal(t, "/test*", expect.RoutePath) assert.Equal(t, "123", expect.RequestID) assert.Equal(t, "https://echo.labstack.com/", expect.Referer) assert.Equal(t, "curl/7.68.0", expect.UserAgent) assert.Equal(t, 418, expect.Status) assert.Equal(t, nil, expect.Error) assert.Equal(t, "32", expect.ContentLength) assert.Equal(t, int64(2), expect.ResponseSize) assert.Len(t, expect.Headers, 1) assert.Equal(t, []string{"curl/7.68.0"}, expect.Headers["User-Agent"]) assert.Len(t, expect.QueryParams, 2) assert.Equal(t, []string{"en"}, expect.QueryParams["lang"]) assert.Equal(t, []string{"1", "2"}, expect.QueryParams["checked"]) assert.Len(t, expect.FormValues, 2) assert.Equal(t, []string{"token"}, expect.FormValues["csrf"]) assert.Equal(t, []string{"1", "2"}, expect.FormValues["multiple"]) } func BenchmarkRequestLogger_withoutMapFields(b *testing.B) { e := echo.New() mw := RequestLoggerWithConfig(RequestLoggerConfig{ Skipper: nil, LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { return nil }, LogLatency: true, LogProtocol: true, LogRemoteIP: true, LogHost: true, LogMethod: true, LogURI: true, LogURIPath: true, LogRoutePath: true, LogRequestID: true, LogReferer: true, LogUserAgent: true, LogStatus: true, LogError: true, LogContentLength: true, LogResponseSize: true, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") return c.String(http.StatusTeapot, "OK") }) req := httptest.NewRequest(http.MethodGet, "/test?lang=en", nil) req.Header.Set("Referer", "https://echo.labstack.com/") req.Header.Set("User-Agent", "curl/7.68.0") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw(c) } } func BenchmarkRequestLogger_withMapFields(b *testing.B) { e := echo.New() mw := RequestLoggerWithConfig(RequestLoggerConfig{ LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error { return nil }, LogLatency: true, LogProtocol: true, LogRemoteIP: true, LogHost: true, LogMethod: true, LogURI: true, LogURIPath: true, LogRoutePath: true, LogRequestID: true, LogReferer: true, LogUserAgent: true, LogStatus: true, LogError: true, LogContentLength: true, LogResponseSize: true, LogHeaders: []string{"accept-encoding", "User-Agent"}, LogQueryParams: []string{"lang", "checked"}, LogFormValues: []string{"csrf", "multiple"}, })(func(c echo.Context) error { c.Request().Header.Set(echo.HeaderXRequestID, "123") c.FormValue("to force parse form") return c.String(http.StatusTeapot, "OK") }) f := make(url.Values) f.Set("csrf", "token") f.Add("multiple", "1") f.Add("multiple", "2") req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode())) req.Header.Set("Referer", "https://echo.labstack.com/") req.Header.Set("User-Agent", "curl/7.68.0") req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { rec := httptest.NewRecorder() c := e.NewContext(req, rec) mw(c) } } golang-github-labstack-echo-4.11.1/middleware/rewrite.go000066400000000000000000000041521445647333300231660ustar00rootroot00000000000000package middleware import ( "regexp" "github.com/labstack/echo/v4" ) type ( // RewriteConfig defines the config for Rewrite middleware. RewriteConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // Rules defines the URL path rewrite rules. The values captured in asterisk can be // retrieved by index e.g. $1, $2 and so on. // Example: // "/old": "/new", // "/api/*": "/$1", // "/js/*": "/public/javascripts/$1", // "/users/*/orders/*": "/user/$1/order/$2", // Required. Rules map[string]string `yaml:"rules"` // RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. // Example: // "^/old/[0.9]+/": "/new", // "^/api/.+?/(.*)": "/v2/$1", RegexRules map[*regexp.Regexp]string `yaml:"regex_rules"` } ) var ( // DefaultRewriteConfig is the default Rewrite middleware config. DefaultRewriteConfig = RewriteConfig{ Skipper: DefaultSkipper, } ) // Rewrite returns a Rewrite middleware. // // Rewrite middleware rewrites the URL path based on the provided rules. func Rewrite(rules map[string]string) echo.MiddlewareFunc { c := DefaultRewriteConfig c.Rules = rules return RewriteWithConfig(c) } // RewriteWithConfig returns a Rewrite middleware with config. // See: `Rewrite()`. func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc { // Defaults if config.Rules == nil && config.RegexRules == nil { panic("echo: rewrite middleware requires url path rewrite rules or regex rules") } if config.Skipper == nil { config.Skipper = DefaultBodyDumpConfig.Skipper } if config.RegexRules == nil { config.RegexRules = make(map[*regexp.Regexp]string) } for k, v := range rewriteRulesRegex(config.Rules) { config.RegexRules[k] = v } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { if config.Skipper(c) { return next(c) } if err := rewriteURL(config.RegexRules, c.Request()); err != nil { return err } return next(c) } } } golang-github-labstack-echo-4.11.1/middleware/rewrite_test.go000066400000000000000000000173611445647333300242330ustar00rootroot00000000000000package middleware import ( "io" "net/http" "net/http/httptest" "net/url" "regexp" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestRewriteAfterRouting(t *testing.T) { e := echo.New() // middlewares added with `Use()` are executed after routing is done and do not affect which route handler is matched e.Use(RewriteWithConfig(RewriteConfig{ Rules: map[string]string{ "/old": "/new", "/api/*": "/$1", "/js/*": "/public/javascripts/$1", "/users/*/orders/*": "/user/$1/order/$2", }, })) e.GET("/public/*", func(c echo.Context) error { return c.String(http.StatusOK, c.Param("*")) }) e.GET("/*", func(c echo.Context) error { return c.String(http.StatusOK, c.Param("*")) }) var testCases = []struct { whenPath string expectRoutePath string expectRequestPath string expectRequestRawPath string }{ { whenPath: "/api/users", expectRoutePath: "api/users", expectRequestPath: "/users", expectRequestRawPath: "", }, { whenPath: "/js/main.js", expectRoutePath: "js/main.js", expectRequestPath: "/public/javascripts/main.js", expectRequestRawPath: "", }, { whenPath: "/users/jack/orders/1", expectRoutePath: "users/jack/orders/1", expectRequestPath: "/user/jack/order/1", expectRequestRawPath: "", }, { // no rewrite rule matched. already encoded URL should not be double encoded or changed in any way whenPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", expectRoutePath: "user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", expectRequestPath: "/user/jill/order/T/cO4lW/t/Vp/", // this is equal to `url.Parse(tc.whenPath)` result expectRequestRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", }, { // just rewrite but do not touch encoding. already encoded URL should not be double encoded whenPath: "/users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F", expectRoutePath: "users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F", expectRequestPath: "/user/jill/order/T/cO4lW/t/Vp/", // this is equal to `url.Parse(tc.whenPath)` result expectRequestRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F", }, { // ` ` (space) is encoded by httpClient to `%20` when doing request to Echo. `%20` should not be double escaped or changed in any way when rewriting request whenPath: "/api/new users", expectRoutePath: "api/new users", expectRequestPath: "/new users", expectRequestRawPath: "", }, } for _, tc := range testCases { t.Run(tc.whenPath, func(t *testing.T) { target, _ := url.Parse(tc.whenPath) req := httptest.NewRequest(http.MethodGet, target.String(), nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, tc.expectRoutePath, rec.Body.String()) assert.Equal(t, tc.expectRequestPath, req.URL.Path) assert.Equal(t, tc.expectRequestRawPath, req.URL.RawPath) }) } } // Issue #1086 func TestEchoRewritePreMiddleware(t *testing.T) { e := echo.New() r := e.Router() // Rewrite old url to new one // middlewares added with `Pre()` are executed before routing is done and therefore change which handler matches e.Pre(Rewrite(map[string]string{ "/old": "/new", }, )) // Route r.Add(http.MethodGet, "/new", func(c echo.Context) error { return c.NoContent(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/old", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "/new", req.URL.EscapedPath()) assert.Equal(t, http.StatusOK, rec.Code) } // Issue #1143 func TestRewriteWithConfigPreMiddleware_Issue1143(t *testing.T) { e := echo.New() r := e.Router() // middlewares added with `Pre()` are executed before routing is done and therefore change which handler matches e.Pre(RewriteWithConfig(RewriteConfig{ Rules: map[string]string{ "/api/*/mgmt/proj/*/agt": "/api/$1/hosts/$2", "/api/*/mgmt/proj": "/api/$1/eng", }, })) r.Add(http.MethodGet, "/api/:version/hosts/:name", func(c echo.Context) error { return c.String(http.StatusOK, "hosts") }) r.Add(http.MethodGet, "/api/:version/eng", func(c echo.Context) error { return c.String(http.StatusOK, "eng") }) for i := 0; i < 100; i++ { req := httptest.NewRequest(http.MethodGet, "/api/v1/mgmt/proj/test/agt", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "/api/v1/hosts/test", req.URL.EscapedPath()) assert.Equal(t, http.StatusOK, rec.Code) defer rec.Result().Body.Close() bodyBytes, _ := io.ReadAll(rec.Result().Body) assert.Equal(t, "hosts", string(bodyBytes)) } } // Issue #1573 func TestEchoRewriteWithCaret(t *testing.T) { e := echo.New() e.Pre(RewriteWithConfig(RewriteConfig{ Rules: map[string]string{ "^/abc/*": "/v1/abc/$1", }, })) rec := httptest.NewRecorder() var req *http.Request req = httptest.NewRequest(http.MethodGet, "/abc/test", nil) e.ServeHTTP(rec, req) assert.Equal(t, "/v1/abc/test", req.URL.Path) req = httptest.NewRequest(http.MethodGet, "/v1/abc/test", nil) e.ServeHTTP(rec, req) assert.Equal(t, "/v1/abc/test", req.URL.Path) req = httptest.NewRequest(http.MethodGet, "/v2/abc/test", nil) e.ServeHTTP(rec, req) assert.Equal(t, "/v2/abc/test", req.URL.Path) } // Verify regex used with rewrite func TestEchoRewriteWithRegexRules(t *testing.T) { e := echo.New() e.Pre(RewriteWithConfig(RewriteConfig{ Rules: map[string]string{ "^/a/*": "/v1/$1", "^/b/*/c/*": "/v2/$2/$1", "^/c/*/*": "/v3/$2", }, RegexRules: map[*regexp.Regexp]string{ regexp.MustCompile("^/x/.+?/(.*)"): "/v4/$1", regexp.MustCompile("^/y/(.+?)/(.*)"): "/v5/$2/$1", }, })) var rec *httptest.ResponseRecorder var req *http.Request testCases := []struct { requestPath string expectPath string }{ {"/unmatched", "/unmatched"}, {"/a/test", "/v1/test"}, {"/b/foo/c/bar/baz", "/v2/bar/baz/foo"}, {"/c/ignore/test", "/v3/test"}, {"/c/ignore1/test/this", "/v3/test/this"}, {"/x/ignore/test", "/v4/test"}, {"/y/foo/bar", "/v5/bar/foo"}, } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { req = httptest.NewRequest(http.MethodGet, tc.requestPath, nil) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectPath, req.URL.EscapedPath()) }) } } // Ensure correct escaping as defined in replacement (issue #1798) func TestEchoRewriteReplacementEscaping(t *testing.T) { e := echo.New() // NOTE: these are incorrect regexps as they do not factor in that URI we are replacing could contain ? (query) and # (fragment) parts // so in reality they append query and fragment part as `$1` matches everything after that prefix e.Pre(RewriteWithConfig(RewriteConfig{ Rules: map[string]string{ "^/a/*": "/$1?query=param", "^/b/*": "/$1;part#one", }, RegexRules: map[*regexp.Regexp]string{ regexp.MustCompile("^/x/(.*)"): "/$1?query=param", regexp.MustCompile("^/y/(.*)"): "/$1;part#one", regexp.MustCompile("^/z/(.*)"): "/$1?test=1#escaped%20test", }, })) var rec *httptest.ResponseRecorder var req *http.Request testCases := []struct { requestPath string expect string }{ {"/unmatched", "/unmatched"}, {"/a/test", "/test?query=param"}, {"/b/foo/bar", "/foo/bar;part#one"}, {"/x/test", "/test?query=param"}, {"/y/foo/bar", "/foo/bar;part#one"}, {"/z/foo/b%20ar", "/foo/b%20ar?test=1#escaped%20test"}, {"/z/foo/b%20ar?nope=1#yes", "/foo/b%20ar?nope=1#yes?test=1%23escaped%20test"}, // example of appending } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { req = httptest.NewRequest(http.MethodGet, tc.requestPath, nil) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expect, req.URL.String()) }) } } golang-github-labstack-echo-4.11.1/middleware/secure.go000066400000000000000000000127451445647333300230020ustar00rootroot00000000000000package middleware import ( "fmt" "github.com/labstack/echo/v4" ) type ( // SecureConfig defines the config for Secure middleware. SecureConfig struct { // Skipper defines a function to skip middleware. Skipper Skipper // XSSProtection provides protection against cross-site scripting attack (XSS) // by setting the `X-XSS-Protection` header. // Optional. Default value "1; mode=block". XSSProtection string `yaml:"xss_protection"` // ContentTypeNosniff provides protection against overriding Content-Type // header by setting the `X-Content-Type-Options` header. // Optional. Default value "nosniff". ContentTypeNosniff string `yaml:"content_type_nosniff"` // XFrameOptions can be used to indicate whether or not a browser should // be allowed to render a page in a ,