pax_global_header00006660000000000000000000000064140212773200014510gustar00rootroot0000000000000052 comment=a97052edaf781a731903d816c9b271028d709131 echo-4.2.1/000077500000000000000000000000001402127732000124325ustar00rootroot00000000000000echo-4.2.1/.editorconfig000066400000000000000000000007231402127732000151110ustar00rootroot00000000000000# 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 echo-4.2.1/.gitattributes000066400000000000000000000012631402127732000153270ustar00rootroot00000000000000# 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 echo-4.2.1/.github/000077500000000000000000000000001402127732000137725ustar00rootroot00000000000000echo-4.2.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000004301402127732000164740ustar00rootroot00000000000000### 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 echo-4.2.1/.github/stale.yml000066400000000000000000000013221402127732000156230ustar00rootroot00000000000000# 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 echo-4.2.1/.github/workflows/000077500000000000000000000000001402127732000160275ustar00rootroot00000000000000echo-4.2.1/.github/workflows/echo.yml000066400000000000000000000057701402127732000175010ustar00rootroot00000000000000name: Run Tests on: push: branches: - master paths: - '**.go' - 'go.*' - '_fixture/**' - '.github/**' - 'codecov.yml' pull_request: branches: - master paths: - '**.go' - 'go.*' - '_fixture/**' - '.github/**' - 'codecov.yml' jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] go: [1.12, 1.13, 1.14, 1.15] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - name: Set GOPATH and PATH run: | echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH shell: bash - name: Set build variables run: | echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV echo "GO111MODULE=on" >> $GITHUB_ENV - name: Checkout Code uses: actions/checkout@v1 with: ref: ${{ github.ref }} - name: Install Dependencies run: go get -v golang.org/x/lint/golint - name: Run Tests run: | golint -set_exit_status ./... go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() && matrix.go == 1.15 && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v1 with: token: fail_ci_if_error: false benchmark: needs: test strategy: matrix: os: [ubuntu-latest] go: [1.15] name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - name: Set GOPATH and PATH run: | echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH shell: bash - name: Set build variables run: | echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV echo "GO111MODULE=on" >> $GITHUB_ENV - name: Checkout Code (Previous) uses: actions/checkout@v2 with: ref: ${{ github.base_ref }} path: previous - name: Checkout Code (New) uses: actions/checkout@v2 with: path: new - name: Install Dependencies run: go get -v golang.org/x/perf/cmd/benchstat - 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 echo-4.2.1/.gitignore000066400000000000000000000000761402127732000144250ustar00rootroot00000000000000.DS_Store coverage.txt _test vendor .idea *.iml *.out .vscode echo-4.2.1/.travis.yml000066400000000000000000000005501402127732000145430ustar00rootroot00000000000000arch: - amd64 - ppc64le language: go go: - 1.14.x - 1.15.x - tip env: - GO111MODULE=on install: - go get -v golang.org/x/lint/golint script: - golint -set_exit_status ./... - go test -race -coverprofile=coverage.txt -covermode=atomic ./... after_success: - bash <(curl -s https://codecov.io/bash) matrix: allow_failures: - go: tip echo-4.2.1/CHANGELOG.md000066400000000000000000000123631402127732000142500ustar00rootroot00000000000000# Changelog ## v4.2.1 - 2020-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 - 2020-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 echo-4.2.1/LICENSE000066400000000000000000000020631402127732000134400ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 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. echo-4.2.1/Makefile000066400000000000000000000017571402127732000141040ustar00rootroot00000000000000PKG := "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 get -u golang.org/x/lint/golint lint: ## Lint the files @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.15" test_version: ## Run tests inside Docker with given version (defaults to 1.15 oldest supported). Example: make test_version goversion=1.15 @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check" echo-4.2.1/README.md000066400000000000000000000075231402127732000137200ustar00rootroot00000000000000 [![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) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/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 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: - 1.9.7+ - 1.10.3+ - 1.14+ 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 ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) 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!") } ``` ## Help - [Forum](https://github.com/labstack/echo/discussions) - [Chat](https://gitter.im/labstack/echo) ## 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 - [Contributors](https://github.com/labstack/echo/graphs/contributors) ## License [MIT](https://github.com/labstack/echo/blob/master/LICENSE) echo-4.2.1/_fixture/000077500000000000000000000000001402127732000142575ustar00rootroot00000000000000echo-4.2.1/_fixture/_fixture/000077500000000000000000000000001402127732000161045ustar00rootroot00000000000000echo-4.2.1/_fixture/_fixture/README.md000066400000000000000000000000651402127732000173640ustar00rootroot00000000000000This directory is used for the static middleware testecho-4.2.1/_fixture/certs/000077500000000000000000000000001402127732000153775ustar00rootroot00000000000000echo-4.2.1/_fixture/certs/README.md000066400000000000000000000006021402127732000166540ustar00rootroot00000000000000To 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 ``` echo-4.2.1/_fixture/certs/cert.pem000066400000000000000000000035161402127732000170440ustar00rootroot00000000000000-----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----- echo-4.2.1/_fixture/certs/key.pem000066400000000000000000000063101402127732000166720ustar00rootroot00000000000000-----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----- echo-4.2.1/_fixture/favicon.ico000066400000000000000000000021761402127732000164060ustar00rootroot00000000000000 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^iecho-4.2.1/_fixture/folder/000077500000000000000000000000001402127732000155325ustar00rootroot00000000000000echo-4.2.1/_fixture/folder/index.html000066400000000000000000000001721402127732000175270ustar00rootroot00000000000000 Echo echo-4.2.1/_fixture/images/000077500000000000000000000000001402127732000155245ustar00rootroot00000000000000echo-4.2.1/_fixture/images/walle.png000066400000000000000000006553551402127732000173610ustar00rootroot00000000000000PNG  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`echo-4.2.1/_fixture/index.html000066400000000000000000000001721402127732000162540ustar00rootroot00000000000000 Echo echo-4.2.1/bind.go000066400000000000000000000242731402127732000137050ustar00rootroot00000000000000package echo import ( "encoding" "encoding/json" "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 = json.NewDecoder(req.Body).Decode(i); err != nil { 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 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 } // 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 } // Issue #1670 - Query params are binded only for GET/DELETE and NOT for usual request with body (POST/PUT/PATCH) // Reasoning here is that parameters in query and bind destination struct could have UNEXPECTED matches and results due that. // i.e. is `&id=1&lang=en` from URL same as `{"id":100,"lang":"de"}` request body and which one should have priority when binding. // This HTTP method check restores pre v4.1.11 behavior and avoids different problems when query is mixed with body if c.Request().Method == http.MethodGet || c.Request().Method == http.MethodDelete { 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 { 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 !structField.CanSet() { continue } structFieldKind := structField.Kind() inputFieldName := typeField.Tag.Get(tag) 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 } echo-4.2.1/bind_test.go000066400000000000000000000751111402127732000147410ustar00rootroot00000000000000package echo import ( "bytes" "encoding/json" "encoding/xml" "errors" "io" "mime/multipart" "net/http" "net/http/httptest" "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 } ) 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) { assert := assert.New(t) testBindOkay(assert, strings.NewReader(userJSON), MIMEApplicationJSON) testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{}) testBindError(assert, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{}) } func TestBindXML(t *testing.T) { assert := assert.New(t) testBindOkay(assert, strings.NewReader(userXML), MIMEApplicationXML) testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New("")) testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{}) testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{}) testBindOkay(assert, strings.NewReader(userXML), MIMETextXML) testBindError(assert, strings.NewReader(invalidContent), MIMETextXML, errors.New("")) testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{}) testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{}) } func TestBindForm(t *testing.T) { assert := assert.New(t) testBindOkay(assert, strings.NewReader(userForm), 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(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 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)) assert := assert.New(t) if assert.NoError(err) { // assert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T) assert.Equal(ts, result.T) assert.Equal(StringArray([]string{"one", "two", "three"}), result.SA) assert.Equal([]Timestamp{ts, ts}, result.TA) assert.Equal(Struct{""}, result.ST) // child struct does not have a field with matching tag assert.Equal("baz", result.StWithTag.Foo) // child struct has field with matching tag } } func TestBindUnmarshalText(t *testing.T) { e := New() req := httptest.NewRequest(GET, "/?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 TestBindUnmarshalTextPtr(t *testing.T) { e := New() req := httptest.NewRequest(GET, "/?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) { body := new(bytes.Buffer) mw := multipart.NewWriter(body) mw.WriteField("id", "1") mw.WriteField("name", "Jon Snow") mw.Close() assert := assert.New(t) testBindOkay(assert, body, mw.FormDataContentType()) } func TestBindUnsupportedMediaType(t *testing.T) { assert := assert.New(t) testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{}) } func TestBindbindData(t *testing.T) { a := assert.New(t) ts := new(bindTestStruct) b := new(DefaultBinder) err := b.bindData(ts, values, "form") a.NoError(err) a.Equal(0, ts.I) a.Equal(int8(0), ts.I8) a.Equal(int16(0), ts.I16) a.Equal(int32(0), ts.I32) a.Equal(int64(0), ts.I64) a.Equal(uint(0), ts.UI) a.Equal(uint8(0), ts.UI8) a.Equal(uint16(0), ts.UI16) a.Equal(uint32(0), ts.UI32) a.Equal(uint64(0), ts.UI64) a.Equal(false, ts.B) a.Equal(float32(0), ts.F32) a.Equal(float64(0), ts.F64) a.Equal("", ts.S) a.Equal("", ts.cantSet) } func TestBindParam(t *testing.T) { e := New() req := httptest.NewRequest(GET, "/", 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(POST, "/", 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) { assert := assert.New(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(err) } assertBindTestStruct(assert, ts) type foo struct { Bar bytes.Buffer } v := &foo{} typ = reflect.TypeOf(v).Elem() val = reflect.ValueOf(v).Elem() assert.Error(setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0))) } func TestBindSetFields(t *testing.T) { assert := assert.New(t) ts := new(bindTestStruct) val := reflect.ValueOf(ts).Elem() // Int if assert.NoError(setIntField("5", 0, val.FieldByName("I"))) { assert.Equal(5, ts.I) } if assert.NoError(setIntField("", 0, val.FieldByName("I"))) { assert.Equal(0, ts.I) } // Uint if assert.NoError(setUintField("10", 0, val.FieldByName("UI"))) { assert.Equal(uint(10), ts.UI) } if assert.NoError(setUintField("", 0, val.FieldByName("UI"))) { assert.Equal(uint(0), ts.UI) } // Float if assert.NoError(setFloatField("15.5", 0, val.FieldByName("F32"))) { assert.Equal(float32(15.5), ts.F32) } if assert.NoError(setFloatField("", 0, val.FieldByName("F32"))) { assert.Equal(float32(0.0), ts.F32) } // Bool if assert.NoError(setBoolField("true", val.FieldByName("B"))) { assert.Equal(true, ts.B) } if assert.NoError(setBoolField("", val.FieldByName("B"))) { assert.Equal(false, ts.B) } ok, err := unmarshalFieldNonPtr("2016-12-06T19:09:05Z", val.FieldByName("T")) if assert.NoError(err) { assert.Equal(ok, true) assert.Equal(Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), ts.T) } } func BenchmarkBindbindDataWithTags(b *testing.B) { b.ReportAllocs() assert := assert.New(b) 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(err) assertBindTestStruct(assert, (*bindTestStruct)(ts)) } func assertBindTestStruct(a *assert.Assertions, ts *bindTestStruct) { a.Equal(0, ts.I) a.Equal(int8(8), ts.I8) a.Equal(int16(16), ts.I16) a.Equal(int32(32), ts.I32) a.Equal(int64(64), ts.I64) a.Equal(uint(0), ts.UI) a.Equal(uint8(8), ts.UI8) a.Equal(uint16(16), ts.UI16) a.Equal(uint32(32), ts.UI32) a.Equal(uint64(64), ts.UI64) a.Equal(true, ts.B) a.Equal(float32(32.5), ts.F32) a.Equal(float64(64.5), ts.F64) a.Equal("test", ts.S) a.Equal("", ts.GetCantSet()) } func testBindOkay(assert *assert.Assertions, r io.Reader, ctype string) { 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) if assert.NoError(err) { assert.Equal(1, u.ID) assert.Equal("Jon Snow", u.Name) } } func testBindError(assert *assert.Assertions, 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(new(HTTPError), err) { assert.Equal(http.StatusBadRequest, err.(*HTTPError).Code) assert.IsType(expectedInternal, err.(*HTTPError).Internal) } default: if assert.IsType(new(HTTPError), err) { assert.Equal(ErrUnsupportedMediaType, err) assert.IsType(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", }, { // binding query params interferes with body. b.BindBody() should be used to bind only body to slice name: "nok, GET query params bind failure - trying to bind json array to slice", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Opts{}, expect: &[]Opts{}, expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct", }, { // 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: "", }, { // binding path params interferes with body. b.BindBody() should be used to bind only body to slice name: "nok, GET path params bind failure - trying to bind json array to slice", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenBindTarget: &[]Opts{}, expect: &[]Opts{}, expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct", }, { 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) }) } } echo-4.2.1/binder.go000066400000000000000000001163721402127732000142360ustar00rootroot00000000000000package echo import ( "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 * UnixTime() - converts unix time (integer) to time.Time * UnixTimeNano() - converts unix time with nano second 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 be 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 be 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 be 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 be 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 } // 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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 be 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, false) } // MustUnixTime requires parameter value to exist to be 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, false) } // UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nano second 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, true) } // MustUnixTimeNano requires parameter value to exist to be 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, true) } func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, isNano 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 } 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 } if isNano { *dest = time.Unix(0, n) } else { *dest = time.Unix(n, 0) } return b } echo-4.2.1/binder_external_test.go000066400000000000000000000073061402127732000171730ustar00rootroot00000000000000// 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 } echo-4.2.1/binder_go1.15_test.go000066400000000000000000000173041402127732000162620ustar00rootroot00000000000000// +build go1.15 package echo /** Since version 1.15 time.Time and time.Duration error message pattern has changed (values are wrapped now in \"\") So pre 1.15 these tests fail with similar error: expected: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param" actual : "code=400, message=failed to bind field value to Duration, internal=time: invalid duration nope, field=param" */ import ( "errors" "github.com/stretchr/testify/assert" "io" "net/http" "net/http/httptest" "testing" "time" ) func createTestContext15(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 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 := createTestContext15(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 := createTestContext15(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 := createTestContext15(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 := createTestContext15(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) } }) } } echo-4.2.1/binder_test.go000066400000000000000000002350151402127732000152710ustar00rootroot00000000000000// run tests as external package to get real feel for API package echo import ( "encoding/json" "errors" "fmt" "github.com/stretchr/testify/assert" "io" "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, err := 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_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_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") } } } echo-4.2.1/codecov.yml000066400000000000000000000002271402127732000146000ustar00rootroot00000000000000coverage: status: project: default: threshold: 1% patch: default: threshold: 1% comment: require_changes: trueecho-4.2.1/context.go000066400000000000000000000402401402127732000144450ustar00rootroot00000000000000package echo import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io" "mime/multipart" "net" "net/http" "net/url" "os" "path/filepath" "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 the request body into provided type `i`. The default binder // does it 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 HTTP error handler. Generally used by middleware. 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 // 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 ( 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 { return ip[:i] } return ip } if ip := c.request.Header.Get(HeaderXRealIP); 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) { enc := json.NewEncoder(c.response) _, pretty := c.QueryParams()["pretty"] if c.echo.Debug || pretty { enc.SetIndent("", " ") } c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(callback + "(")); err != nil { return } if err = enc.Encode(i); err != nil { return } if _, err = c.response.Write([]byte(");")); err != nil { return } return } func (c *context) json(code int, i interface{}, indent string) error { enc := json.NewEncoder(c.response) if indent != "" { enc.SetIndent("", indent) } c.writeContentType(MIMEApplicationJSONCharsetUTF8) c.response.Status = code return enc.Encode(i) } 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) File(file string) (err error) { f, err := os.Open(file) if err != nil { return NotFoundHandler(c) } defer f.Close() fi, _ := f.Stat() if fi.IsDir() { file = filepath.Join(file, indexPage) f, err = os.Open(file) if err != nil { return NotFoundHandler(c) } defer f.Close() if fi, err = f.Stat(); err != nil { return } } http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) 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] = "" } } echo-4.2.1/context_test.go000066400000000000000000000552001402127732000155060ustar00rootroot00000000000000package 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(POST, "/", 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(POST, "/", 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(POST, "/", 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() r.Add(http.MethodGet, "/users/:id", nil) 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", nil) 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(GET, "/?"+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(GET, "/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(POST, "/", 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.0.1", }, { &context{ request: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{"192.168.0.1"}, }, }, }, "192.168.0.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()) } } echo-4.2.1/echo.go000066400000000000000000000701731402127732000137070ustar00rootroot00000000000000/* 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 ( "bytes" stdContext "context" "crypto/tls" "errors" "fmt" "io" "io/ioutil" stdLog "log" "net" "net/http" "net/url" "os" "path/filepath" "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. Echo struct { 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 StdLogger *stdLog.Logger colorer *color.Color premiddleware []MiddlewareFunc middleware []MiddlewareFunc maxParam *int router *Router routers map[string]*Router notFoundHandler HandlerFunc pool sync.Pool 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 Validator Validator Renderer Renderer Logger Logger IPExtractor IPExtractor ListenerNetwork string } // 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(HandlerFunc) HandlerFunc // HandlerFunc defines a function to serve HTTP requests. HandlerFunc func(Context) error // HTTPErrorHandler is a centralized HTTP error handler. HTTPErrorHandler func(error, Context) // Validator is the interface that wraps the Validate function. Validator interface { Validate(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" ) // Headers const ( HeaderAccept = "Accept" HeaderAcceptEncoding = "Accept-Encoding" 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" 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" HeaderXRequestedWith = "X-Requested-With" HeaderServer = "Server" HeaderOrigin = "Origin" // 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.2.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 ( ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) ErrNotFound = NewHTTPError(http.StatusNotFound) ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) ErrForbidden = NewHTTPError(http.StatusForbidden) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) ErrTooManyRequests = NewHTTPError(http.StatusTooManyRequests) ErrBadRequest = NewHTTPError(http.StatusBadRequest) ErrBadGateway = NewHTTPError(http.StatusBadGateway) ErrInternalServerError = NewHTTPError(http.StatusInternalServerError) ErrRequestTimeout = NewHTTPError(http.StatusRequestTimeout) ErrServiceUnavailable = NewHTTPError(http.StatusServiceUnavailable) 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 { return ErrMethodNotAllowed } ) // New creates an instance of Echo. func New() (e *Echo) { e = &Echo{ 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.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. func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { 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 if m, ok := he.Message.(string); ok { if e.Debug { message = Map{"message": m, "error": err.Error()} } else { message = Map{"message": m} } } // Send response if !c.Response().Committed { 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...) } // Any registers a new route for all HTTP methods and path with matching handler // in the router with optional route-level middleware. 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 } // Static registers a new route with path prefix to serve static files from the // provided root directory. func (e *Echo) Static(prefix, root string) *Route { if root == "" { root = "." // For security we want to restrict to CWD. } return e.static(prefix, root, e.GET) } func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route { h := func(c Context) error { p, err := url.PathUnescape(c.Param("*")) if err != nil { return err } name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security fi, err := os.Stat(name) if err != nil { // The access path does not exist return NotFoundHandler(c) } // 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() && p[len(p)-1] != '/' { // Redirect to ends with "/" return c.Redirect(http.StatusMovedPermanently, p+"/") } return c.File(name) } // Handle added routes based on trailing slash: // /prefix => exact route "/prefix" + any route "/prefix/*" // /prefix/ => only any route "/prefix/*" if prefix != "" { if prefix[len(prefix)-1] == '/' { // Only add any route for intentional trailing slash return get(prefix+"*", h) } get(prefix, h) } return get(prefix+"/*", h) } 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, middleware ...MiddlewareFunc) *Route { name := handlerName(handler) router := e.findRouter(host) router.Add(method, path, func(c Context) error { h := applyMiddleware(handler, middleware...) return h(c) }) r := &Route{ Method: method, Path: path, Name: name, } e.router.routes[method+path] = r return r } // 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 a 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 an URL from route name and provided parameters. func (e *Echo) Reverse(name string, params ...interface{}) string { uri := new(bytes.Buffer) ln := len(params) n := 0 for _, r := range e.router.routes { if r.Name == name { for i, l := 0, len(r.Path); i < l; i++ { if (r.Path[i] == ':' || r.Path[i] == '*') && n < ln { for ; i < l && r.Path[i] != '/'; i++ { } uri.WriteString(fmt.Sprintf("%v", params[n])) n++ } if i < l { uri.WriteByte(r.Path[i]) } } break } } return uri.String() } // Routes returns the registered routes. func (e *Echo) Routes() []*Route { routes := make([]*Route, 0, len(e.router.routes)) for _, v := range e.router.routes { routes = append(routes, v) } return 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) h := NotFoundHandler 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 ioutil.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) (err 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 { e.Listener, err = newListener(s.Addr, e.ListenerNetwork) if err != nil { return err } } 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) (err 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 { e.Listener, err = newListener(s.Addr, e.ListenerNetwork) if err != nil { e.startupMutex.Unlock() return err } } 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 } // 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 } echo-4.2.1/echo_go1.13_test.go000066400000000000000000000011031402127732000157210ustar00rootroot00000000000000// +build go1.13 package echo import ( "errors" "net/http" "testing" "github.com/stretchr/testify/assert" ) 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("internal", 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()) }) } echo-4.2.1/echo_test.go000066400000000000000000000770331402127732000147500ustar00rootroot00000000000000package echo import ( "bytes" stdContext "context" "crypto/tls" "errors" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "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"` Name string `json:"name" xml:"name" form:"name" query:"name" param:"name"` } ) const ( userJSON = `{"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 ` 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: "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) { assert := assert.New(t) e := New() // HandlerFunc e.Static("/static", "_fixture") errCh := make(chan error) go func() { errCh <- e.Start("127.0.0.1:1323") }() time.Sleep(200 * time.Millisecond) if resp, err := http.Get("http://127.0.0.1:1323/static"); err == nil { defer resp.Body.Close() assert.Equal(http.StatusOK, resp.StatusCode) if body, err := ioutil.ReadAll(resp.Body); err == nil { assert.Equal(true, strings.HasPrefix(string(body), "")) } else { assert.Fail(err.Error()) } } else { assert.Fail(err.Error()) } if err := e.Close(); err != nil { t.Fatal(err) } } func TestEchoFile(t *testing.T) { e := New() e.File("/walle", "_fixture/images/walle.png") c, b := request(http.MethodGet, "/walle", e) assert.Equal(t, http.StatusOK, c) assert.NotEmpty(t, 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) w.Write([]byte("test")) })) 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 := assert.New(t) assert.Equal("/static/file", e.URL(static)) assert.Equal("/users/:id", e.URL(getUser)) assert.Equal("/users/1", e.URL(getUser, "1")) assert.Equal("/users/1", e.URL(getUser, "1")) assert.Equal("/documents/foo.txt", e.URL(getAny, "foo.txt")) assert.Equal("/documents/*", e.URL(getAny)) assert.Equal("/group/users/1/files/:fid", e.URL(getFile, "1")) assert.Equal("/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 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 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 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) } 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 := ioutil.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := ioutil.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, 0) 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, 0) 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", 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()) }) } func TestDefaultHTTPErrorHandler(t *testing.T) { e := New() e.Debug = true e.Any("/plain", func(c Context) error { return errors.New("An error occurred") }) e.Any("/badrequest", func(c Context) error { return NewHTTPError(http.StatusBadRequest, "Invalid request") }) e.Any("/servererror", func(c Context) error { return NewHTTPError(http.StatusInternalServerError, map[string]interface{}{ "code": 33, "message": "Something bad happened", "error": "stackinfo", }) }) // With Debug=true plain response contains error message c, b := request(http.MethodGet, "/plain", e) assert.Equal(t, http.StatusInternalServerError, c) assert.Equal(t, "{\n \"error\": \"An error occurred\",\n \"message\": \"Internal Server Error\"\n}\n", b) // and special handling for HTTPError c, b = request(http.MethodGet, "/badrequest", e) assert.Equal(t, http.StatusBadRequest, c) assert.Equal(t, "{\n \"error\": \"code=400, message=Invalid request\",\n \"message\": \"Invalid request\"\n}\n", b) // complex errors are serialized to pretty JSON c, b = request(http.MethodGet, "/servererror", e) assert.Equal(t, http.StatusInternalServerError, c) assert.Equal(t, "{\n \"code\": 33,\n \"error\": \"stackinfo\",\n \"message\": \"Something bad happened\"\n}\n", b) e.Debug = false // With Debug=false the error response is shortened c, b = request(http.MethodGet, "/plain", e) assert.Equal(t, http.StatusInternalServerError, c) assert.Equal(t, "{\"message\":\"Internal Server Error\"}\n", b) c, b = request(http.MethodGet, "/badrequest", e) assert.Equal(t, http.StatusBadRequest, c) assert.Equal(t, "{\"message\":\"Invalid request\"}\n", b) // No difference for error response with non plain string errors c, b = request(http.MethodGet, "/servererror", e) assert.Equal(t, http.StatusInternalServerError, c) assert.Equal(t, "{\"code\":33,\"error\":\"stackinfo\",\"message\":\"Something bad happened\"}\n", 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 resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) if body, err := ioutil.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 TestEchoReverse(t *testing.T) { assert := assert.New(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/*" assert.Equal("/static", e.Reverse("/static")) assert.Equal("/static", e.Reverse("/static", "missing param")) assert.Equal("/static/*", e.Reverse("/static/*")) assert.Equal("/static/foo.txt", e.Reverse("/static/*", "foo.txt")) assert.Equal("/params/:foo", e.Reverse("/params/:foo")) assert.Equal("/params/one", e.Reverse("/params/:foo", "one")) assert.Equal("/params/:foo/bar/:qux", e.Reverse("/params/:foo/bar/:qux")) assert.Equal("/params/one/bar/:qux", e.Reverse("/params/:foo/bar/:qux", "one")) assert.Equal("/params/one/bar/two", e.Reverse("/params/:foo/bar/:qux", "one", "two")) assert.Equal("/params/one/bar/two/three", e.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three")) } 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 := ioutil.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := ioutil.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 := ioutil.ReadFile("_fixture/certs/cert.pem") require.NoError(t, err) key, err := ioutil.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) } echo-4.2.1/go.mod000066400000000000000000000010251402127732000135360ustar00rootroot00000000000000module github.com/labstack/echo/v4 go 1.15 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/labstack/gommon v0.3.0 github.com/mattn/go-colorable v0.1.7 // indirect github.com/stretchr/testify v1.4.0 github.com/valyala/fasttemplate v1.2.1 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect golang.org/x/text v0.3.3 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 ) echo-4.2.1/go.sum000066400000000000000000000121171402127732000135670ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= echo-4.2.1/group.go000066400000000000000000000105101402127732000141120ustar00rootroot00000000000000package 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 } // Allow all requests to reach the group as they might get dropped if router // doesn't find a match, making none of the group middleware process. g.Any("", NotFoundHandler) g.Any("/*", 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 } // Static implements `Echo#Static()` for sub-routes within the Group. func (g *Group) Static(prefix, root string) { g.static(prefix, root, g.GET) } // File implements `Echo#File()` for sub-routes within the Group. func (g *Group) File(path, file string) { g.file(path, file, g.GET) } // 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...) } echo-4.2.1/group_test.go000066400000000000000000000055531402127732000151640ustar00rootroot00000000000000package echo import ( "io/ioutil" "net/http" "net/http/httptest" "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 := ioutil.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) } echo-4.2.1/ip.go000066400000000000000000000073211402127732000133740ustar00rootroot00000000000000package echo import ( "net" "net/http" "strings" ) 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 } 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 func(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 { directIP := ExtractIPDirect()(req) realIP := req.Header.Get(HeaderXRealIP) if realIP != "" { if ip := net.ParseIP(directIP); ip != nil && checker.trust(ip) { return realIP } } return directIP } } // 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 := ExtractIPDirect()(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-- { ip := net.ParseIP(strings.TrimSpace(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]) } } echo-4.2.1/ip_test.go000066400000000000000000000224231402127732000144330ustar00rootroot00000000000000package echo import ( "net" "net/http" "strings" "testing" testify "github.com/stretchr/testify/assert" ) const ( // For RemoteAddr ipForRemoteAddrLoopback = "127.0.0.1" // From 127.0.0.0/8 sampleRemoteAddrLoopback = ipForRemoteAddrLoopback + ":8080" ipForRemoteAddrExternal = "203.0.113.1" sampleRemoteAddrExternal = ipForRemoteAddrExternal + ":8080" // For x-real-ip ipForRealIP = "203.0.113.10" // For XFF ipForXFF1LinkLocal = "169.254.0.101" // From 169.254.0.0/16 ipForXFF2Private = "192.168.0.102" // From 192.168.0.0/16 ipForXFF3External = "2001:db8::103" ipForXFF4Private = "fc00::104" // From fc00::/7 ipForXFF5External = "198.51.100.105" ipForXFF6External = "192.0.2.106" ipForXFFBroken = "this.is.broken.lol" // keys for test cases ipTestReqKeyNoHeader = "no header" ipTestReqKeyRealIPExternal = "x-real-ip; remote addr external" ipTestReqKeyRealIPInternal = "x-real-ip; remote addr internal" ipTestReqKeyRealIPAndXFFExternal = "x-real-ip and xff; remote addr external" ipTestReqKeyRealIPAndXFFInternal = "x-real-ip and xff; remote addr internal" ipTestReqKeyXFFExternal = "xff; remote addr external" ipTestReqKeyXFFInternal = "xff; remote addr internal" ipTestReqKeyBrokenXFF = "broken xff" ) var ( sampleXFF = strings.Join([]string{ ipForXFF6External, ipForXFF5External, ipForXFF4Private, ipForXFF3External, ipForXFF2Private, ipForXFF1LinkLocal, }, ", ") requests = map[string]*http.Request{ ipTestReqKeyNoHeader: &http.Request{ RemoteAddr: sampleRemoteAddrExternal, }, ipTestReqKeyRealIPExternal: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{ipForRealIP}, }, RemoteAddr: sampleRemoteAddrExternal, }, ipTestReqKeyRealIPInternal: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{ipForRealIP}, }, RemoteAddr: sampleRemoteAddrLoopback, }, ipTestReqKeyRealIPAndXFFExternal: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{ipForRealIP}, HeaderXForwardedFor: []string{sampleXFF}, }, RemoteAddr: sampleRemoteAddrExternal, }, ipTestReqKeyRealIPAndXFFInternal: &http.Request{ Header: http.Header{ "X-Real-Ip": []string{ipForRealIP}, HeaderXForwardedFor: []string{sampleXFF}, }, RemoteAddr: sampleRemoteAddrLoopback, }, ipTestReqKeyXFFExternal: &http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{sampleXFF}, }, RemoteAddr: sampleRemoteAddrExternal, }, ipTestReqKeyXFFInternal: &http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{sampleXFF}, }, RemoteAddr: sampleRemoteAddrLoopback, }, ipTestReqKeyBrokenXFF: &http.Request{ Header: http.Header{ HeaderXForwardedFor: []string{ipForXFFBroken + ", " + ipForXFF1LinkLocal}, }, RemoteAddr: sampleRemoteAddrLoopback, }, } ) func TestExtractIP(t *testing.T) { _, ipv4AllRange, _ := net.ParseCIDR("0.0.0.0/0") _, ipv6AllRange, _ := net.ParseCIDR("::/0") _, ipForXFF3ExternalRange, _ := net.ParseCIDR(ipForXFF3External + "/48") _, ipForRemoteAddrExternalRange, _ := net.ParseCIDR(ipForRemoteAddrExternal + "/24") tests := map[string]*struct { extractor IPExtractor expectedIPs map[string]string }{ "ExtractIPDirect": { ExtractIPDirect(), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPAndXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromRealIPHeader(default)": { ExtractIPFromRealIPHeader(), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRealIP, ipTestReqKeyRealIPAndXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPAndXFFInternal: ipForRealIP, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromRealIPHeader(trust only direct-facing proxy)": { ExtractIPFromRealIPHeader(TrustLoopback(false), TrustLinkLocal(false), TrustPrivateNet(false), TrustIPRange(ipForRemoteAddrExternalRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRealIP, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForRealIP, ipTestReqKeyRealIPAndXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromRealIPHeader(trust direct-facing proxy)": { ExtractIPFromRealIPHeader(TrustIPRange(ipForRemoteAddrExternalRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRealIP, ipTestReqKeyRealIPInternal: ipForRealIP, ipTestReqKeyRealIPAndXFFExternal: ipForRealIP, ipTestReqKeyRealIPAndXFFInternal: ipForRealIP, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromXFFHeader(default)": { ExtractIPFromXFFHeader(), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPAndXFFInternal: ipForXFF3External, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForXFF3External, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromXFFHeader(trust only direct-facing proxy)": { ExtractIPFromXFFHeader(TrustLoopback(false), TrustLinkLocal(false), TrustPrivateNet(false), TrustIPRange(ipForRemoteAddrExternalRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForXFF1LinkLocal, ipTestReqKeyRealIPAndXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyXFFExternal: ipForXFF1LinkLocal, ipTestReqKeyXFFInternal: ipForRemoteAddrLoopback, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromXFFHeader(trust direct-facing proxy)": { ExtractIPFromXFFHeader(TrustIPRange(ipForRemoteAddrExternalRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForXFF3External, ipTestReqKeyRealIPAndXFFInternal: ipForXFF3External, ipTestReqKeyXFFExternal: ipForXFF3External, ipTestReqKeyXFFInternal: ipForXFF3External, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromXFFHeader(trust everything)": { // This is similar to legacy behavior, but ignores x-real-ip header. ExtractIPFromXFFHeader(TrustIPRange(ipv4AllRange), TrustIPRange(ipv6AllRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForXFF6External, ipTestReqKeyRealIPAndXFFInternal: ipForXFF6External, ipTestReqKeyXFFExternal: ipForXFF6External, ipTestReqKeyXFFInternal: ipForXFF6External, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, "ExtractIPFromXFFHeader(trust ipForXFF3External)": { // This trusts private network also after "additional" trust ranges unlike `TrustNProxies(1)` doesn't ExtractIPFromXFFHeader(TrustIPRange(ipForXFF3ExternalRange)), map[string]string{ ipTestReqKeyNoHeader: ipForRemoteAddrExternal, ipTestReqKeyRealIPExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPInternal: ipForRemoteAddrLoopback, ipTestReqKeyRealIPAndXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyRealIPAndXFFInternal: ipForXFF5External, ipTestReqKeyXFFExternal: ipForRemoteAddrExternal, ipTestReqKeyXFFInternal: ipForXFF5External, ipTestReqKeyBrokenXFF: ipForRemoteAddrLoopback, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { assert := testify.New(t) for key, req := range requests { actual := test.extractor(req) expected := test.expectedIPs[key] assert.Equal(expected, actual, "Request: %s", key) } }) } } echo-4.2.1/log.go000066400000000000000000000016371402127732000135510ustar00rootroot00000000000000package 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{}) } ) echo-4.2.1/middleware/000077500000000000000000000000001402127732000145475ustar00rootroot00000000000000echo-4.2.1/middleware/basic_auth.go000066400000000000000000000050521402127732000172020ustar00rootroot00000000000000package middleware import ( "encoding/base64" "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) { b, err := base64.StdEncoding.DecodeString(auth[l+1:]) if err != nil { return 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 } } } echo-4.2.1/middleware/basic_auth_test.go000066400000000000000000000037521402127732000202460ustar00rootroot00000000000000package 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") }) assert := assert.New(t) // Valid credentials auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret")) req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(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(h(c)) // Case-insensitive header scheme auth = strings.ToUpper(basic) + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret")) req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(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(http.StatusUnauthorized, he.Code) assert.Equal(basic+` realm="someRealm"`, res.Header().Get(echo.HeaderWWWAuthenticate)) // Missing Authorization header req.Header.Del(echo.HeaderAuthorization) he = h(c).(*echo.HTTPError) assert.Equal(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(http.StatusUnauthorized, he.Code) } echo-4.2.1/middleware/body_dump.go000066400000000000000000000046651402127732000170730ustar00rootroot00000000000000package middleware import ( "bufio" "bytes" "io" "io/ioutil" "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, _ = ioutil.ReadAll(c.Request().Body) } c.Request().Body = ioutil.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() } echo-4.2.1/middleware/body_dump_test.go000066400000000000000000000036161402127732000201250ustar00rootroot00000000000000package middleware import ( "errors" "io/ioutil" "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 := ioutil.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) }) assert := assert.New(t) if assert.NoError(mw(h)(c)) { assert.Equal(requestBody, hw) assert.Equal(responseBody, hw) assert.Equal(http.StatusOK, rec.Code) assert.Equal(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() } }) } echo-4.2.1/middleware/body_limit.go000066400000000000000000000053101402127732000172300ustar00rootroot00000000000000package 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} }, } } echo-4.2.1/middleware/body_limit_test.go000066400000000000000000000044171402127732000202760ustar00rootroot00000000000000package middleware import ( "bytes" "io/ioutil" "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 := ioutil.ReadAll(c.Request().Body) if err != nil { return err } return c.String(http.StatusOK, string(body)) } assert := assert.New(t) // Based on content length (within limit) if assert.NoError(BodyLimit("2M")(h)(c)) { assert.Equal(http.StatusOK, rec.Code) assert.Equal(hw, rec.Body.Bytes()) } // Based on content read (overlimit) he := BodyLimit("2B")(h)(c).(*echo.HTTPError) assert.Equal(http.StatusRequestEntityTooLarge, he.Code) // Based on content read (within limit) req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) rec = httptest.NewRecorder() c = e.NewContext(req, rec) if assert.NoError(BodyLimit("2M")(h)(c)) { assert.Equal(http.StatusOK, rec.Code) assert.Equal("Hello, World!", rec.Body.String()) } // Based on content read (overlimit) req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw)) rec = httptest.NewRecorder() c = e.NewContext(req, rec) he = BodyLimit("2B")(h)(c).(*echo.HTTPError) assert.Equal(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: ioutil.NopCloser(bytes.NewReader(hw)), context: e.NewContext(req, rec), } // read all should return ErrStatusRequestEntityTooLarge _, err := ioutil.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(ioutil.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec)) n, err := reader.Read(bt) assert.Equal(t, 2, n) assert.Equal(t, nil, err) } echo-4.2.1/middleware/compress.go000066400000000000000000000065551402127732000167440ustar00rootroot00000000000000package middleware import ( "bufio" "compress/gzip" "io" "io/ioutil" "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"` } gzipResponseWriter struct { io.Writer http.ResponseWriter } ) const ( gzipScheme = "gzip" ) var ( // DefaultGzipConfig is the default Gzip middleware config. DefaultGzipConfig = GzipConfig{ Skipper: DefaultSkipper, Level: -1, } ) // 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 } pool := gzipCompressPool(config) 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) { res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806 i := pool.Get() w, ok := i.(*gzip.Writer) if !ok { return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error()) } rw := res.Writer w.Reset(rw) defer func() { if res.Size == 0 { if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { res.Header().Del(echo.HeaderContentEncoding) } // 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(ioutil.Discard) } w.Close() pool.Put(w) }() grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw} res.Writer = grw } return next(c) } } } func (w *gzipResponseWriter) WriteHeader(code int) { if code == http.StatusNoContent { // Issue #489 w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) } w.Header().Del(echo.HeaderContentLength) // Issue #444 w.ResponseWriter.WriteHeader(code) } func (w *gzipResponseWriter) Write(b []byte) (int, error) { if w.Header().Get(echo.HeaderContentType) == "" { w.Header().Set(echo.HeaderContentType, http.DetectContentType(b)) } return w.Writer.Write(b) } func (w *gzipResponseWriter) Flush() { 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(ioutil.Discard, config.Level) if err != nil { return err } return w }, } } echo-4.2.1/middleware/compress_test.go000066400000000000000000000115531402127732000177750ustar00rootroot00000000000000package middleware import ( "bytes" "compress/gzip" "io" "io/ioutil" "net/http" "net/http/httptest" "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 := assert.New(t) assert.Equal("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(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) assert.Contains(rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain) r, err := gzip.NewReader(rec.Body) if assert.NoError(err) { buf := new(bytes.Buffer) defer r.Close() buf.ReadFrom(r) assert.Equal("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(rec.Flushed) assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding)) r.Reset(rec.Body) _, err = io.ReadFull(r, chunkBuf) assert.NoError(err) assert.Equal("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(err) assert.Equal("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("test", buf.String()) } 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 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 := ioutil.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) } } echo-4.2.1/middleware/cors.go000066400000000000000000000142141402127732000160460ustar00rootroot00000000000000package 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 // AllowOrigin defines a list of origins that may access the resource. // Optional. Default value []string{"*"}. 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. // Optional. AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"` // AllowMethods defines a list methods allowed when accessing the resource. // This is used in response to a preflight request. // Optional. Default value DefaultCORSConfig.AllowMethods. AllowMethods []string `yaml:"allow_methods"` // AllowHeaders defines a list of request headers that can be used when // making the actual request. This is in response to a preflight request. // Optional. Default value []string{}. AllowHeaders []string `yaml:"allow_headers"` // AllowCredentials indicates whether or not the response to the request // can be exposed when the credentials flag 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. // Optional. Default value false. AllowCredentials bool `yaml:"allow_credentials"` // ExposeHeaders defines a whitelist headers that clients are allowed to // access. // Optional. Default value []string{}. ExposeHeaders []string `yaml:"expose_headers"` // MaxAge indicates how long (in seconds) the results of a preflight request // can be cached. // Optional. Default value 0. 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: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_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 } if len(config.AllowMethods) == 0 { config.AllowMethods = DefaultCORSConfig.AllowMethods } allowOriginPatterns := []string{} for _, origin := range config.AllowOrigins { pattern := regexp.QuoteMeta(origin) pattern = strings.Replace(pattern, "\\*", ".*", -1) pattern = strings.Replace(pattern, "\\?", ".", -1) 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 := "" preflight := req.Method == http.MethodOptions res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) // No Origin provided 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 { allowOrigin = origin break } if o == "*" || o == origin { allowOrigin = o break } if matchSubdomain(origin, o) { allowOrigin = origin break } } // Check allowed origin patterns for _, re := range allowOriginPatterns { if allowOrigin == "" { didx := strings.Index(origin, "://") if didx == -1 { continue } domAuth := origin[didx+3:] // to avoid regex cost by invalid long domain if len(domAuth) > 253 { break } 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) } // Simple request if !preflight { res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin) if config.AllowCredentials { res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") } 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) res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin) res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods) if config.AllowCredentials { res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") } 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) } } } echo-4.2.1/middleware/cors_test.go000066400000000000000000000301061402127732000171030ustar00rootroot00000000000000package middleware import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestCORS(t *testing.T) { e := echo.New() // Wildcard origin req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) h := CORS()(echo.NotFoundHandler) req.Header.Set(echo.HeaderOrigin, "localhost") h(c) assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) // Wildcard AllowedOrigin with no Origin header in request req = httptest.NewRequest(http.MethodGet, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h = CORS()(echo.NotFoundHandler) h(c) assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin) // Allow origins req = httptest.NewRequest(http.MethodGet, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) h = CORSWithConfig(CORSConfig{ AllowOrigins: []string{"localhost"}, AllowCredentials: true, MaxAge: 3600, })(echo.NotFoundHandler) req.Header.Set(echo.HeaderOrigin, "localhost") h(c) assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials)) // Preflight request req = httptest.NewRequest(http.MethodOptions, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, "localhost") req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) cors := CORSWithConfig(CORSConfig{ AllowOrigins: []string{"localhost"}, AllowCredentials: true, MaxAge: 3600, }) h = cors(echo.NotFoundHandler) h(c) assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods)) assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials)) assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge)) // Preflight request with `AllowOrigins` * req = httptest.NewRequest(http.MethodOptions, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, "localhost") req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) cors = CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, AllowCredentials: true, MaxAge: 3600, }) h = cors(echo.NotFoundHandler) h(c) assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods)) assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials)) assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge)) // Preflight request with Access-Control-Request-Headers req = httptest.NewRequest(http.MethodOptions, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, "localhost") req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.Header.Set(echo.HeaderAccessControlRequestHeaders, "Special-Request-Header") cors = CORSWithConfig(CORSConfig{ AllowOrigins: []string{"*"}, }) h = cors(echo.NotFoundHandler) h(c) assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) assert.Equal(t, "Special-Request-Header", rec.Header().Get(echo.HeaderAccessControlAllowHeaders)) assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods)) // Preflight request with `AllowOrigins` which allow all subdomains with * req = httptest.NewRequest(http.MethodOptions, "/", nil) rec = httptest.NewRecorder() c = e.NewContext(req, rec) req.Header.Set(echo.HeaderOrigin, "http://aaa.example.com") cors = CORSWithConfig(CORSConfig{ AllowOrigins: []string{"http://*.example.com"}, }) h = cors(echo.NotFoundHandler) h(c) assert.Equal(t, "http://aaa.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) req.Header.Set(echo.HeaderOrigin, "http://bbb.example.com") h(c) assert.Equal(t, "http://bbb.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) } 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 TestCorsHeaders(t *testing.T) { tests := []struct { domain, allowedOrigin, method string expected bool }{ { domain: "", // Request does not have Origin header allowedOrigin: "*", method: http.MethodGet, expected: false, }, { domain: "http://example.com", allowedOrigin: "*", method: http.MethodGet, expected: true, }, { domain: "", // Request does not have Origin header allowedOrigin: "http://example.com", method: http.MethodGet, expected: false, }, { domain: "http://bar.com", allowedOrigin: "http://example.com", method: http.MethodGet, expected: false, }, { domain: "http://example.com", allowedOrigin: "http://example.com", method: http.MethodGet, expected: true, }, { domain: "", // Request does not have Origin header allowedOrigin: "*", method: http.MethodOptions, expected: false, }, { domain: "http://example.com", allowedOrigin: "*", method: http.MethodOptions, expected: true, }, { domain: "", // Request does not have Origin header allowedOrigin: "http://example.com", method: http.MethodOptions, expected: false, }, { domain: "http://bar.com", allowedOrigin: "http://example.com", method: http.MethodGet, expected: false, }, { domain: "http://example.com", allowedOrigin: "http://example.com", method: http.MethodOptions, expected: true, }, } e := echo.New() for _, tt := range tests { req := httptest.NewRequest(tt.method, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) if tt.domain != "" { req.Header.Set(echo.HeaderOrigin, tt.domain) } cors := CORSWithConfig(CORSConfig{ AllowOrigins: []string{tt.allowedOrigin}, //AllowCredentials: true, //MaxAge: 3600, }) h := cors(echo.NotFoundHandler) h(c) assert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary)) expectedAllowOrigin := "" if tt.allowedOrigin == "*" { expectedAllowOrigin = "*" } else { expectedAllowOrigin = tt.domain } switch { case tt.expected && tt.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 tt.expected && tt.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 } if tt.method == http.MethodOptions { assert.Equal(t, http.StatusNoContent, rec.Code) } } } 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)) } } } echo-4.2.1/middleware/csrf.go000066400000000000000000000141131402127732000160330ustar00rootroot00000000000000package middleware import ( "crypto/subtle" "errors" "net/http" "strings" "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 ":" that is used // to extract token from the request. // Optional. Default value "header:X-CSRF-Token". // Possible values: // - "header:" // - "form:" // - "query:" 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"` } // csrfTokenExtractor defines a function that takes `echo.Context` and returns // either a token or an error. csrfTokenExtractor func(echo.Context) (string, error) ) 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 == SameSiteNoneMode { config.CookieSecure = true } // Initialize parts := strings.Split(config.TokenLookup, ":") extractor := csrfTokenFromHeader(parts[1]) switch parts[0] { case "form": extractor = csrfTokenFromForm(parts[1]) case "query": extractor = csrfTokenFromQuery(parts[1]) } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() k, err := c.Cookie(config.CookieName) token := "" // Generate token if err != nil { token = random.String(config.TokenLength) } else { // Reuse token token = k.Value } switch req.Method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace: default: // Validate token only for requests which are not defined as 'safe' by RFC7231 clientToken, err := extractor(c) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if !validateCSRFToken(token, clientToken) { return echo.NewHTTPError(http.StatusForbidden, "invalid csrf token") } } // 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) } } } // csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the // provided request header. func csrfTokenFromHeader(header string) csrfTokenExtractor { return func(c echo.Context) (string, error) { return c.Request().Header.Get(header), nil } } // csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the // provided form parameter. func csrfTokenFromForm(param string) csrfTokenExtractor { return func(c echo.Context) (string, error) { token := c.FormValue(param) if token == "" { return "", errors.New("missing csrf token in the form parameter") } return token, nil } } // csrfTokenFromQuery returns a `csrfTokenExtractor` that extracts token from the // provided query parameter. func csrfTokenFromQuery(param string) csrfTokenExtractor { return func(c echo.Context) (string, error) { token := c.QueryParam(param) if token == "" { return "", errors.New("missing csrf token in the query string") } return token, nil } } func validateCSRFToken(token, clientToken string) bool { return subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1 } echo-4.2.1/middleware/csrf_samesite.go000066400000000000000000000003161402127732000177250ustar00rootroot00000000000000// +build go1.13 package middleware import ( "net/http" ) const ( // SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524) SameSiteNoneMode http.SameSite = http.SameSiteNoneMode ) echo-4.2.1/middleware/csrf_samesite_1.12.go000066400000000000000000000002731402127732000203700ustar00rootroot00000000000000// +build !go1.13 package middleware import ( "net/http" ) const ( // SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524) SameSiteNoneMode http.SameSite = 4 ) echo-4.2.1/middleware/csrf_samesite_test.go000066400000000000000000000013371402127732000207700ustar00rootroot00000000000000// +build go1.13 package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) // Test for SameSiteModeNone moved to separate file for Go 1.12 support 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: 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"]) } echo-4.2.1/middleware/csrf_test.go000066400000000000000000000066701402127732000171030ustar00rootroot00000000000000package middleware import ( "fmt" "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(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) csrf := CSRFWithConfig(CSRFConfig{ TokenLength: 16, }) 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(16) 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 TestCSRFTokenFromForm(t *testing.T) { f := make(url.Values) f.Set("csrf", "token") e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) c := e.NewContext(req, nil) token, err := csrfTokenFromForm("csrf")(c) if assert.NoError(t, err) { assert.Equal(t, "token", token) } _, err = csrfTokenFromForm("invalid")(c) assert.Error(t, err) } func TestCSRFTokenFromQuery(t *testing.T) { q := make(url.Values) q.Set("csrf", "token") e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) req.URL.RawQuery = q.Encode() c := e.NewContext(req, nil) token, err := csrfTokenFromQuery("csrf")(c) if assert.NoError(t, err) { assert.Equal(t, "token", token) } _, err = csrfTokenFromQuery("invalid")(c) assert.Error(t, err) csrfTokenFromQuery("csrf") } 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) fmt.Println(rec.Header()["Set-Cookie"]) assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"]) } echo-4.2.1/middleware/decompress.go000066400000000000000000000057231402127732000172510ustar00rootroot00000000000000package middleware import ( "bytes" "compress/gzip" "io" "io/ioutil" "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{} { // create with an empty reader (but with GZIP header) w, err := gzip.NewWriterLevel(ioutil.Discard, gzip.BestSpeed) if err != nil { return err } b := new(bytes.Buffer) w.Reset(b) w.Flush() w.Close() r, err := gzip.NewReader(bytes.NewReader(b.Bytes())) if err != nil { return err } return r }, } } //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) } switch c.Request().Header.Get(echo.HeaderContentEncoding) { case GZIPEncoding: b := c.Request().Body i := pool.Get() gr, ok := i.(*gzip.Reader) if !ok { return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error()) } if err := gr.Reset(b); err != nil { pool.Put(gr) if err == io.EOF { //ignore if body is empty return next(c) } return err } var buf bytes.Buffer io.Copy(&buf, gr) gr.Close() pool.Put(gr) b.Close() // http.Request.Body is closed by the Server, but because we are replacing it, it must be closed here r := ioutil.NopCloser(&buf) c.Request().Body = r } return next(c) } } } echo-4.2.1/middleware/decompress_test.go000066400000000000000000000133321402127732000203030ustar00rootroot00000000000000package middleware import ( "bytes" "compress/gzip" "errors" "io/ioutil" "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 := assert.New(t) assert.Equal("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(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) b, err := ioutil.ReadAll(req.Body) assert.NoError(err) assert.Equal(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 := assert.New(t) assert.Equal("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(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) b, err := ioutil.ReadAll(req.Body) assert.NoError(err) assert.Equal(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 := ioutil.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 := ioutil.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 := ioutil.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 } echo-4.2.1/middleware/jwt.go000066400000000000000000000172531402127732000157120ustar00rootroot00000000000000package middleware import ( "fmt" "net/http" "reflect" "strings" "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" ) 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. 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 // Signing key to validate token. Used as fallback if SigningKeys has length 0. // Required. This or SigningKeys. SigningKey interface{} // Map of signing keys to validate token with kid field usage. // Required. This or SigningKey. SigningKeys map[string]interface{} // Signing method, used to check token signing method. // 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. // Optional. Default value jwt.MapClaims Claims jwt.Claims // TokenLookup is a string in the form of ":" that is used // to extract token from the request. // Optional. Default value "header:Authorization". // Possible values: // - "header:" // - "query:" // - "param:" // - "cookie:" // - "form:" TokenLookup string // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". AuthScheme string keyFunc jwt.Keyfunc } // JWTSuccessHandler defines a function which is executed for a valid token. JWTSuccessHandler func(echo.Context) // JWTErrorHandler defines a function which is executed for an invalid token. JWTErrorHandler func(error) error // JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context. JWTErrorHandlerWithContext func(error, echo.Context) error jwtExtractor func(echo.Context) (string, 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, AuthScheme: "Bearer", Claims: jwt.MapClaims{}, } ) // 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` func JWT(key interface{}) echo.MiddlewareFunc { c := DefaultJWTConfig c.SigningKey = key return JWTWithConfig(c) } // JWTWithConfig returns a JWT auth middleware with config. // See: `JWT()`. func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultJWTConfig.Skipper } if config.SigningKey == nil && len(config.SigningKeys) == 0 { 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 == "" { config.TokenLookup = DefaultJWTConfig.TokenLookup } if config.AuthScheme == "" { config.AuthScheme = DefaultJWTConfig.AuthScheme } config.keyFunc = func(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 } // Initialize parts := strings.Split(config.TokenLookup, ":") extractor := jwtFromHeader(parts[1], config.AuthScheme) switch parts[0] { case "query": extractor = jwtFromQuery(parts[1]) case "param": extractor = jwtFromParam(parts[1]) case "cookie": extractor = jwtFromCookie(parts[1]) case "form": extractor = jwtFromForm(parts[1]) } 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) } auth, err := extractor(c) if err != nil { if config.ErrorHandler != nil { return config.ErrorHandler(err) } if config.ErrorHandlerWithContext != nil { return config.ErrorHandlerWithContext(err, c) } return err } token := new(jwt.Token) // 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 && token.Valid { // Store user information from token into context. c.Set(config.ContextKey, token) if config.SuccessHandler != nil { config.SuccessHandler(c) } return next(c) } if config.ErrorHandler != nil { return config.ErrorHandler(err) } if config.ErrorHandlerWithContext != nil { return config.ErrorHandlerWithContext(err, c) } return &echo.HTTPError{ Code: ErrJWTInvalid.Code, Message: ErrJWTInvalid.Message, Internal: err, } } } } // jwtFromHeader returns a `jwtExtractor` that extracts token from the request header. func jwtFromHeader(header string, authScheme string) jwtExtractor { return func(c echo.Context) (string, error) { auth := c.Request().Header.Get(header) l := len(authScheme) if len(auth) > l+1 && auth[:l] == authScheme { return auth[l+1:], nil } return "", ErrJWTMissing } } // jwtFromQuery returns a `jwtExtractor` that extracts token from the query string. func jwtFromQuery(param string) jwtExtractor { return func(c echo.Context) (string, error) { token := c.QueryParam(param) if token == "" { return "", ErrJWTMissing } return token, nil } } // jwtFromParam returns a `jwtExtractor` that extracts token from the url param string. func jwtFromParam(param string) jwtExtractor { return func(c echo.Context) (string, error) { token := c.Param(param) if token == "" { return "", ErrJWTMissing } return token, nil } } // jwtFromCookie returns a `jwtExtractor` that extracts token from the named cookie. func jwtFromCookie(name string) jwtExtractor { return func(c echo.Context) (string, error) { cookie, err := c.Cookie(name) if err != nil { return "", ErrJWTMissing } return cookie.Value, nil } } // jwtFromForm returns a `jwtExtractor` that extracts token from the form field. func jwtFromForm(name string) jwtExtractor { return func(c echo.Context) (string, error) { field := c.FormValue(name) if field == "" { return "", ErrJWTMissing } return field, nil } } echo-4.2.1/middleware/jwt_test.go000066400000000000000000000243471402127732000167530ustar00rootroot00000000000000package middleware import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/dgrijalva/jwt-go" "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 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 TestJWT(t *testing.T) { e := echo.New() 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 for _, tc := range []struct { 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 info string }{ { expPanic: true, info: "No signing key provided", }, { expErrCode: http.StatusBadRequest, config: JWTConfig{ SigningKey: validKey, SigningMethod: "RS256", }, info: "Unexpected signing method", }, { expErrCode: http.StatusUnauthorized, hdrAuth: validAuth, config: JWTConfig{SigningKey: invalidKey}, info: "Invalid key", }, { hdrAuth: validAuth, config: JWTConfig{SigningKey: validKey}, info: "Valid JWT", }, { hdrAuth: "Token" + " " + token, config: JWTConfig{AuthScheme: "Token", SigningKey: validKey}, info: "Valid JWT with custom AuthScheme", }, { hdrAuth: validAuth, config: JWTConfig{ Claims: &jwtCustomClaims{}, SigningKey: []byte("secret"), }, info: "Valid JWT with custom claims", }, { hdrAuth: "invalid-auth", expErrCode: http.StatusBadRequest, config: JWTConfig{SigningKey: validKey}, info: "Invalid Authorization header", }, { config: JWTConfig{SigningKey: validKey}, expErrCode: http.StatusBadRequest, info: "Empty header auth field", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwt=" + token, info: "Valid query method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwtxyz=" + token, expErrCode: http.StatusBadRequest, info: "Invalid query param name", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b&jwt=invalid-token", expErrCode: http.StatusUnauthorized, info: "Invalid query param value", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "query:jwt", }, reqURL: "/?a=b", expErrCode: http.StatusBadRequest, info: "Empty query", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "param:jwt", }, reqURL: "/" + token, info: "Valid param method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, hdrCookie: "jwt=" + token, info: "Valid cookie method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, expErrCode: http.StatusUnauthorized, hdrCookie: "jwt=invalid", info: "Invalid token with cookie method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "cookie:jwt", }, expErrCode: http.StatusBadRequest, info: "Empty cookie", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, formValues: map[string]string{"jwt": token}, info: "Valid form method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, expErrCode: http.StatusUnauthorized, formValues: map[string]string{"jwt": "invalid"}, info: "Invalid token with form method", }, { config: JWTConfig{ SigningKey: validKey, TokenLookup: "form:jwt", }, expErrCode: http.StatusBadRequest, info: "Empty form field", }, } { 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.info) continue } 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 TestJWTwithKID(t *testing.T) { test := assert.New(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) test.Equal(tc.expErrCode, he.Code, tc.info) continue } h := JWTWithConfig(tc.config)(handler) if test.NoError(h(c), tc.info) { user := c.Get("user").(*jwt.Token) switch claims := user.Claims.(type) { case jwt.MapClaims: test.Equal(claims["name"], "John Doe", tc.info) case *jwtCustomClaims: test.Equal(claims.Name, "John Doe", tc.info) test.Equal(claims.Admin, true, tc.info) default: panic("unexpected type of claims") } } } } echo-4.2.1/middleware/key_auth.go000066400000000000000000000076451402127732000167230ustar00rootroot00000000000000package middleware import ( "errors" "net/http" "strings" "github.com/labstack/echo/v4" ) 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 ":" that is used // to extract key from the request. // Optional. Default value "header:Authorization". // Possible values: // - "header:" // - "query:" // - "form:" KeyLookup string `yaml:"key_lookup"` // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". AuthScheme string // Validator is a function to validate key. // Required. Validator KeyAuthValidator } // KeyAuthValidator defines a function to validate KeyAuth credentials. KeyAuthValidator func(string, echo.Context) (bool, error) keyExtractor func(echo.Context) (string, error) ) var ( // DefaultKeyAuthConfig is the default KeyAuth middleware config. DefaultKeyAuthConfig = KeyAuthConfig{ Skipper: DefaultSkipper, KeyLookup: "header:" + echo.HeaderAuthorization, AuthScheme: "Bearer", } ) // 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") } // Initialize parts := strings.Split(config.KeyLookup, ":") extractor := keyFromHeader(parts[1], config.AuthScheme) switch parts[0] { case "query": extractor = keyFromQuery(parts[1]) case "form": extractor = keyFromForm(parts[1]) } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } // Extract and verify key key, err := extractor(c) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } valid, err := config.Validator(key, c) if err != nil { return &echo.HTTPError{ Code: http.StatusUnauthorized, Message: "invalid key", Internal: err, } } else if valid { return next(c) } return echo.ErrUnauthorized } } } // keyFromHeader returns a `keyExtractor` that extracts key from the request header. func keyFromHeader(header string, authScheme string) keyExtractor { return func(c echo.Context) (string, error) { auth := c.Request().Header.Get(header) if auth == "" { return "", errors.New("missing key in request header") } if header == echo.HeaderAuthorization { l := len(authScheme) if len(auth) > l+1 && auth[:l] == authScheme { return auth[l+1:], nil } return "", errors.New("invalid key in the request header") } return auth, nil } } // keyFromQuery returns a `keyExtractor` that extracts key from the query string. func keyFromQuery(param string) keyExtractor { return func(c echo.Context) (string, error) { key := c.QueryParam(param) if key == "" { return "", errors.New("missing key in the query string") } return key, nil } } // keyFromForm returns a `keyExtractor` that extracts key from the form. func keyFromForm(param string) keyExtractor { return func(c echo.Context) (string, error) { key := c.FormValue(param) if key == "" { return "", errors.New("missing key in the form") } return key, nil } } echo-4.2.1/middleware/key_auth_test.go000066400000000000000000000037151402127732000177540ustar00rootroot00000000000000package middleware import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestKeyAuth(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) config := KeyAuthConfig{ Validator: func(key string, c echo.Context) (bool, error) { return key == "valid-key", nil }, } h := KeyAuthWithConfig(config)(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) assert := assert.New(t) // Valid key auth := DefaultKeyAuthConfig.AuthScheme + " " + "valid-key" req.Header.Set(echo.HeaderAuthorization, auth) assert.NoError(h(c)) // Invalid key auth = DefaultKeyAuthConfig.AuthScheme + " " + "invalid-key" req.Header.Set(echo.HeaderAuthorization, auth) he := h(c).(*echo.HTTPError) assert.Equal(http.StatusUnauthorized, he.Code) // Missing Authorization header req.Header.Del(echo.HeaderAuthorization) he = h(c).(*echo.HTTPError) assert.Equal(http.StatusBadRequest, he.Code) // Key from custom header config.KeyLookup = "header:API-Key" h = KeyAuthWithConfig(config)(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) req.Header.Set("API-Key", "valid-key") assert.NoError(h(c)) // Key from query string config.KeyLookup = "query:key" h = KeyAuthWithConfig(config)(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) q := req.URL.Query() q.Add("key", "valid-key") req.URL.RawQuery = q.Encode() assert.NoError(h(c)) // Key from form config.KeyLookup = "form:key" h = KeyAuthWithConfig(config)(func(c echo.Context) error { return c.String(http.StatusOK, "test") }) f := make(url.Values) f.Set("key", "valid-key") req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) c = e.NewContext(req, rec) assert.NoError(h(c)) } echo-4.2.1/middleware/logger.go000066400000000000000000000135071402127732000163630ustar00rootroot00000000000000package 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_nano // - time_rfc3339 // - time_rfc3339_nano // - time_custom // - id (Request ID) // - remote_ip // - uri // - host // - method // - path // - protocol // - referer // - user_agent // - status // - error // - latency (In nanoseconds) // - latency_human (Human readable) // - bytes_in (Bytes received) // - bytes_out (Bytes sent) // - header: // - query: // - form: // // Example "${remote_ip} ${status}" // // Optional. Default value DefaultLoggerConfig.Format. Format string `yaml:"format"` // Optional. Default value DefaultLoggerConfig.CustomTimeFormat. CustomTimeFormat string `yaml:"custom_time_format"` // 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 "time_unix": return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 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 "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 } } } echo-4.2.1/middleware/logger_test.go000066400000000000000000000122741402127732000174220ustar00rootroot00000000000000package middleware import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "net/url" "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}", "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("/", func(c echo.Context) error { return c.String(http.StatusOK, "Header Logged") }) req := httptest.NewRequest(http.MethodGet, "/?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\":\"/\"": 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) } echo-4.2.1/middleware/method_override.go000066400000000000000000000050001402127732000202500ustar00rootroot00000000000000package 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) } } echo-4.2.1/middleware/method_override_test.go000066400000000000000000000027261402127732000213230ustar00rootroot00000000000000package 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) } echo-4.2.1/middleware/middleware.go000066400000000000000000000037111402127732000172150ustar00rootroot00000000000000package middleware import ( "net/http" "net/url" "regexp" "strconv" "strings" "github.com/labstack/echo/v4" ) type ( // Skipper defines a function to skip middleware. Returning true skips processing // the middleware. Skipper func(echo.Context) bool // BeforeFunc defines a function which is executed just before the middleware. BeforeFunc func(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.Replace(k, `\*`, "(.*?)", -1) if strings.HasPrefix(k, `\^`) { k = strings.Replace(k, `\^`, "^", -1) } k = k + "$" rulesRegex[regexp.MustCompile(k)] = v } return rulesRegex } func rewritePath(rewriteRegex map[*regexp.Regexp]string, req *http.Request) { for k, v := range rewriteRegex { rawPath := req.URL.RawPath if rawPath != "" { // RawPath is only set when there has been escaping done. In that case Path must be deduced from rewritten RawPath // because encoded Path could match rules that RawPath did not if replacer := captureTokens(k, rawPath); replacer != nil { rawPath = replacer.Replace(v) req.URL.RawPath = rawPath req.URL.Path, _ = url.PathUnescape(rawPath) return // rewrite only once } continue } if replacer := captureTokens(k, req.URL.Path); replacer != nil { req.URL.Path = replacer.Replace(v) return // rewrite only once } } } // DefaultSkipper returns false which processes the middleware. func DefaultSkipper(echo.Context) bool { return false } echo-4.2.1/middleware/middleware_test.go000066400000000000000000000037221402127732000202560ustar00rootroot00000000000000package middleware import ( "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "regexp" "testing" ) func TestRewritePath(t *testing.T) { var testCases = []struct { whenURL string expectPath string expectRawPath 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: "", }, { whenURL: "http://localhost:8080/users/%20a/orders/%20aa", expectPath: "/user/ a/order/ aa", expectRawPath: "", }, { whenURL: "http://localhost:8080/%47%6f%2f", expectPath: "/Go/", expectRawPath: "/%47%6f%2f", }, { 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", }, } rules := map[*regexp.Regexp]string{ regexp.MustCompile("^/old$"): "/new", regexp.MustCompile("^/users/(.*?)/orders/(.*?)$"): "/user/$1/order/$2", } for _, tc := range testCases { t.Run(tc.whenURL, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) rewritePath(rules, req) 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. }) } } echo-4.2.1/middleware/proxy.go000066400000000000000000000155371402127732000162720ustar00rootroot00000000000000package middleware import ( "fmt" "io" "math/rand" "net" "net/http" "net/url" "regexp" "sync" "sync/atomic" "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 // 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 } commonBalancer struct { targets []*ProxyTarget mutex sync.RWMutex } // RandomBalancer implements a random load balancing technique. randomBalancer struct { *commonBalancer random *rand.Rand } // RoundRobinBalancer implements a round-robin load balancing technique. roundRobinBalancer struct { *commonBalancer i uint32 } ) 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.Sprintf("proxy raw, hijack error=%v, url=%s", t.URL, err)) 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", t.URL, err))) 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", t.URL, err))) 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=%v, url=%s", t.URL, err)) } }) } // NewRandomBalancer returns a random proxy balancer. func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer { b := &randomBalancer{commonBalancer: new(commonBalancer)} b.targets = targets return b } // NewRoundRobinBalancer returns a round-robin proxy balancer. func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer { b := &roundRobinBalancer{commonBalancer: new(commonBalancer)} b.targets = targets return b } // AddTarget adds an upstream target to the list. func (b *commonBalancer) AddTarget(target *ProxyTarget) bool { for _, t := range b.targets { if t.Name == target.Name { return false } } b.mutex.Lock() defer b.mutex.Unlock() b.targets = append(b.targets, target) return true } // RemoveTarget removes an upstream target from the list. 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. func (b *randomBalancer) Next(c echo.Context) *ProxyTarget { if b.random == nil { b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) } b.mutex.RLock() defer b.mutex.RUnlock() return b.targets[b.random.Intn(len(b.targets))] } // Next returns an upstream target using round-robin technique. func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget { b.i = b.i % uint32(len(b.targets)) t := b.targets[b.i] atomic.AddUint32(&b.i, 1) return t } // 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 { // Defaults if config.Skipper == nil { config.Skipper = DefaultProxyConfig.Skipper } if config.Balancer == nil { panic("echo: proxy middleware requires balancer") } 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 } } 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() tgt := config.Balancer.Next(c) c.Set(config.ContextKey, tgt) // Set rewrite path and raw path rewritePath(config.RegexRewrite, req) // 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()) } // 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) } if e, ok := c.Get("_error").(error); ok { err = e } return } } } echo-4.2.1/middleware/proxy_1_11.go000066400000000000000000000036061402127732000170050ustar00rootroot00000000000000// +build go1.11 package middleware import ( "context" "fmt" "net/http" "net/http/httputil" "strings" "github.com/labstack/echo/v4" ) // 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 } echo-4.2.1/middleware/proxy_1_11_n.go000066400000000000000000000003721402127732000173170ustar00rootroot00000000000000// +build !go1.11 package middleware import ( "net/http" "net/http/httputil" "github.com/labstack/echo/v4" ) func proxyHTTP(t *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler { return httputil.NewSingleHostReverseProxy(t.URL) } echo-4.2.1/middleware/proxy_1_11_test.go000066400000000000000000000035461402127732000200470ustar00rootroot00000000000000// +build go1.11 package middleware import ( "context" "net/http" "net/http/httptest" "net/url" "sync" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestProxy_1_11(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) rec := httptest.NewRecorder() // 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 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) } echo-4.2.1/middleware/proxy_test.go000066400000000000000000000175031402127732000173240ustar00rootroot00000000000000package middleware import ( "bytes" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "net/url" "regexp" "testing" "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 = ioutil.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) } 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 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}}) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() // 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"}, } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { req.URL, _ = url.Parse(tc.requestPath) rec = httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, tc.expectPath, req.URL.EscapedPath()) assert.Equal(t, tc.statusCode, rec.Code) }) } } echo-4.2.1/middleware/rate_limiter.go000066400000000000000000000176471402127732000175750ustar00rootroot00000000000000package 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 burst int expiresIn time.Duration lastCleanup 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). Burst and ExpiresIn will be set to default values. 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 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.Minutes}, ) */ 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.lastCleanup = now() return } // RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore type RateLimiterMemoryStoreConfig struct { Rate rate.Limit // Rate of requests allowed to pass as req/s Burst int // Burst 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 } limiter.lastSeen = now() if now().Sub(store.lastCleanup) > store.expiresIn { store.cleanupStaleVisitors() } store.mutex.Unlock() return limiter.AllowN(now(), 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 now().Sub(visitor.lastSeen) > store.expiresIn { delete(store.visitors, id) } } store.lastCleanup = now() } /* actual time method which is mocked in test file */ var now = time.Now echo-4.2.1/middleware/rate_limiter_test.go000066400000000000000000000273211402127732000206220ustar00rootroot00000000000000package middleware import ( "errors" "fmt" "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) now = 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}) now = time.Now fmt.Println(now()) inMemoryStore.visitors = map[string]*Visitor{ "A": { Limiter: rate.NewLimiter(1, 3), lastSeen: now(), }, "B": { Limiter: rate.NewLimiter(1, 3), lastSeen: now().Add(-1 * time.Minute), }, "C": { Limiter: rate.NewLimiter(1, 3), lastSeen: now().Add(-5 * time.Minute), }, "D": { Limiter: rate.NewLimiter(1, 3), lastSeen: 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) } echo-4.2.1/middleware/recover.go000066400000000000000000000050311402127732000165420ustar00rootroot00000000000000package middleware import ( "fmt" "runtime" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" ) type ( // 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 } ) var ( // DefaultRecoverConfig is the default Recover middleware config. DefaultRecoverConfig = RecoverConfig{ Skipper: DefaultSkipper, StackSize: 4 << 10, // 4 KB DisableStackAll: false, DisablePrintStack: false, LogLevel: 0, } ) // 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) error { if config.Skipper(c) { return next(c) } defer func() { if r := recover(); r != nil { err, ok := r.(error) if !ok { err = fmt.Errorf("%v", r) } stack := make([]byte, config.StackSize) length := runtime.Stack(stack, !config.DisableStackAll) 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) } } c.Error(err) } }() return next(c) } } } echo-4.2.1/middleware/recover_test.go000066400000000000000000000033521402127732000176050ustar00rootroot00000000000000package middleware import ( "bytes" "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") })) h(c) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(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)) } }) } } echo-4.2.1/middleware/redirect.go000066400000000000000000000110341402127732000166760ustar00rootroot00000000000000package middleware import ( "net/http" "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) (ok bool, url string) { if ok = scheme != "https"; ok { url = "https://" + host + uri } return }) } // 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) (ok bool, url string) { if ok = scheme != "https" && host[:4] != www; ok { url = "https://www." + host + uri } return }) } // 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 ok = scheme != "https"; ok { if host[:4] == www { host = host[4:] } url = "https://" + host + uri } return }) } // 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) (ok bool, url string) { if ok = host[:4] != www; ok { url = scheme + "://www." + host + uri } return }) } // 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) (ok bool, url string) { if ok = host[:4] == www; ok { url = scheme + "://" + host[4:] + uri } return }) } func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc { if config.Skipper == nil { config.Skipper = DefaultTrailingSlashConfig.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) } } } echo-4.2.1/middleware/redirect_test.go000066400000000000000000000051131402127732000177360ustar00rootroot00000000000000package 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) { res := redirectTest(HTTPSRedirect, "labstack.com", nil) assert.Equal(t, http.StatusMovedPermanently, res.Code) assert.Equal(t, "https://labstack.com/", res.Header().Get(echo.HeaderLocation)) } func TestHTTPSRedirectBehindTLSTerminationProxy(t *testing.T) { header := http.Header{} header.Set(echo.HeaderXForwardedProto, "https") res := redirectTest(HTTPSRedirect, "labstack.com", header) assert.Equal(t, http.StatusOK, res.Code) } func TestRedirectHTTPSWWWRedirect(t *testing.T) { res := redirectTest(HTTPSWWWRedirect, "labstack.com", nil) assert.Equal(t, http.StatusMovedPermanently, res.Code) assert.Equal(t, "https://www.labstack.com/", res.Header().Get(echo.HeaderLocation)) } func TestRedirectHTTPSWWWRedirectBehindTLSTerminationProxy(t *testing.T) { header := http.Header{} header.Set(echo.HeaderXForwardedProto, "https") res := redirectTest(HTTPSWWWRedirect, "labstack.com", header) assert.Equal(t, http.StatusOK, res.Code) } func TestRedirectHTTPSNonWWWRedirect(t *testing.T) { res := redirectTest(HTTPSNonWWWRedirect, "www.labstack.com", nil) assert.Equal(t, http.StatusMovedPermanently, res.Code) assert.Equal(t, "https://labstack.com/", res.Header().Get(echo.HeaderLocation)) } func TestRedirectHTTPSNonWWWRedirectBehindTLSTerminationProxy(t *testing.T) { header := http.Header{} header.Set(echo.HeaderXForwardedProto, "https") res := redirectTest(HTTPSNonWWWRedirect, "www.labstack.com", header) assert.Equal(t, http.StatusOK, res.Code) } func TestRedirectWWWRedirect(t *testing.T) { res := redirectTest(WWWRedirect, "labstack.com", nil) assert.Equal(t, http.StatusMovedPermanently, res.Code) assert.Equal(t, "http://www.labstack.com/", res.Header().Get(echo.HeaderLocation)) } func TestRedirectNonWWWRedirect(t *testing.T) { res := redirectTest(NonWWWRedirect, "www.labstack.com", nil) assert.Equal(t, http.StatusMovedPermanently, res.Code) assert.Equal(t, "http://labstack.com/", 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 } echo-4.2.1/middleware/request_id.go000066400000000000000000000026431402127732000172470ustar00rootroot00000000000000package 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 } ) var ( // DefaultRequestIDConfig is the default RequestID middleware config. DefaultRequestIDConfig = RequestIDConfig{ Skipper: DefaultSkipper, Generator: generator, } ) // 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 } 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(echo.HeaderXRequestID) if rid == "" { rid = config.Generator() } res.Header().Set(echo.HeaderXRequestID, rid) return next(c) } } } func generator() string { return random.String(32) } echo-4.2.1/middleware/request_id_test.go000066400000000000000000000023401402127732000203000ustar00rootroot00000000000000package 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 rid = RequestIDWithConfig(RequestIDConfig{ Generator: func() string { return "customGenerator" }, }) h = rid(handler) h(c) assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator") } 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), "") } echo-4.2.1/middleware/rewrite.go000066400000000000000000000041611402127732000165610ustar00rootroot00000000000000package 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) } req := c.Request() // Set rewrite path and raw path rewritePath(config.RegexRules, req) return next(c) } } } echo-4.2.1/middleware/rewrite_test.go000066400000000000000000000164551402127732000176310ustar00rootroot00000000000000package middleware import ( "io/ioutil" "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, _ := ioutil.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() 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", }, })) var rec *httptest.ResponseRecorder var req *http.Request testCases := []struct { requestPath string expectPath 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"}, } 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.Path) }) } } echo-4.2.1/middleware/secure.go000066400000000000000000000127451402127732000163750ustar00rootroot00000000000000package 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 ,