Merge branch 'master' into feature/serverconfig

This commit is contained in:
迷茫少年 2021-08-17 14:49:44 +08:00 committed by GitHub
commit 841578e649
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 5643 additions and 2329 deletions

View File

@ -3,11 +3,47 @@
- Please provide source code and commit sha if you found a bug. - Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them. - Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:8201/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:8201/hello/world
<YOUR RESULT>
```
## Environment
- go version: - go version:
- gin version (or commit ref): - gin version (or commit ref):
- operating system: - operating system:
## Description
## Screenshots

View File

@ -1,7 +1,7 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integrations systems such as TravisCI. - It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.

62
.github/workflows/gin.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Run Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: [1.13, 1.14, 1.15, 1.16]
test-tags: ['', nomsgpack]
name: ${{ matrix.os }} @ Go ${{ matrix.go }} ${{ matrix.test-tags }}
runs-on: ${{ matrix.os }}
env:
GO111MODULE: on
TESTTAGS: ${{ matrix.test-tags }}
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout Code
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Install Dependencies
run: make tools
- name: Run Check
run: |
make vet
make fmt-check
make misspell-check
- name: Run Tests
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
notification-gitter:
needs: test
runs-on: ubuntu-latest
steps:
- name: Notification failure message
if: failure()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" -d level=error https://webhooks.gitter.im/e/7f95bf605c4d356372f4
- name: Notification success message
if: success()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" https://webhooks.gitter.im/e/7f95bf605c4d356372f4

View File

@ -1,44 +0,0 @@
language: go
matrix:
fast_finish: true
include:
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: master
env: GO111MODULE=on
git:
depth: 10
before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install:
- if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
go_import_path: github.com/gin-gonic/gin
script:
- make vet
- make fmt-check
- make misspell-check
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

View File

@ -156,7 +156,7 @@ People and companies, who have contributed, in alphabetical order.
- Fix variadic parameter in the flexible render API - Fix variadic parameter in the flexible render API
- Fix Corrupted plain render - Fix Corrupted plain render
- Add Pluggable View Renderer Example - Add Pluggable View Renderer Example
**@msemenistyi (Mykyta Semenistyi)** **@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method - update Readme.md. Add code to String method
@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
**@rogierlommers (Rogier Lommers)** **@rogierlommers (Rogier Lommers)**
- Add updated static serve example - Add updated static serve example
**@rw-access (Ross Wolf)**
- Added support to mix exact and param routes
**@se77en (Damon Zhao)** **@se77en (Damon Zhao)**
- Improve color logging - Improve color logging

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,163 @@
# Gin ChangeLog
### Gin 1.4.0 ## Gin v1.7.3
### BUGFIXES
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
## Gin v1.7.2
### BUGFIXES
* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
## Gin v1.7.1
### BUGFIXES
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
## Gin v1.7.0
### BUGFIXES
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
### ENHANCEMENTS
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
* remove a unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
## Gin v1.6.3
### ENHANCEMENTS
* Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
## Gin v1.6.2
### BUGFIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1
### BUGFIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0
### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUGFIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
### ENHANCEMENTS
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
### DOCS
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
## Gin v1.5.0
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
- [FIX] Some code improvements [#1909](https://github.com/gin-gonic/gin/pull/1909)
- [FIX] Use encode replace json marshal increase json encoder speed [#1546](https://github.com/gin-gonic/gin/pull/1546)
- [NEW] Hold matched route full path in the Context [#1826](https://github.com/gin-gonic/gin/pull/1826)
- [FIX] Fix context.Params race condition on Copy() [#1841](https://github.com/gin-gonic/gin/pull/1841)
- [NEW] Add context param query cache [#1450](https://github.com/gin-gonic/gin/pull/1450)
- [FIX] Improve GetQueryMap performance [#1918](https://github.com/gin-gonic/gin/pull/1918)
- [FIX] Improve get post data [#1920](https://github.com/gin-gonic/gin/pull/1920)
- [FIX] Use context instead of x/net/context [#1922](https://github.com/gin-gonic/gin/pull/1922)
- [FIX] Attempt to fix PostForm cache bug [#1931](https://github.com/gin-gonic/gin/pull/1931)
- [NEW] Add support of multipart multi files [#1949](https://github.com/gin-gonic/gin/pull/1949)
- [NEW] Support bind http header param [#1957](https://github.com/gin-gonic/gin/pull/1957)
- [FIX] Drop support for go1.8 and go1.9 [#1933](https://github.com/gin-gonic/gin/pull/1933)
- [FIX] Bugfix for the FullPath feature [#1919](https://github.com/gin-gonic/gin/pull/1919)
- [FIX] Gin1.5 bytes.Buffer to strings.Builder [#1939](https://github.com/gin-gonic/gin/pull/1939)
- [FIX] Upgrade github.com/ugorji/go/codec [#1969](https://github.com/gin-gonic/gin/pull/1969)
- [NEW] Support bind unix time [#1980](https://github.com/gin-gonic/gin/pull/1980)
- [FIX] Simplify code [#2004](https://github.com/gin-gonic/gin/pull/2004)
- [NEW] Support negative Content-Length in DataFromReader [#1981](https://github.com/gin-gonic/gin/pull/1981)
- [FIX] Identify terminal on a RISC-V architecture for auto-colored logs [#2019](https://github.com/gin-gonic/gin/pull/2019)
- [BREAKING] `Context.JSONP()` now expects a semicolon (`;`) at the end [#2007](https://github.com/gin-gonic/gin/pull/2007)
- [BREAKING] Upgrade default `binding.Validator` to v9 (see [its changelog](https://github.com/go-playground/validator/releases/tag/v9.0.0)) [#1015](https://github.com/gin-gonic/gin/pull/1015)
- [NEW] Add `DisallowUnknownFields()` in `Context.BindJSON()` [#2028](https://github.com/gin-gonic/gin/pull/2028)
- [NEW] Use specific `net.Listener` with `Engine.RunListener()` [#2023](https://github.com/gin-gonic/gin/pull/2023)
- [FIX] Fix some typo [#2079](https://github.com/gin-gonic/gin/pull/2079) [#2080](https://github.com/gin-gonic/gin/pull/2080)
- [FIX] Relocate binding body tests [#2086](https://github.com/gin-gonic/gin/pull/2086)
- [FIX] Use Writer in Context.Status [#1606](https://github.com/gin-gonic/gin/pull/1606)
- [FIX] `Engine.RunUnix()` now returns the error if it can't change the file mode [#2093](https://github.com/gin-gonic/gin/pull/2093)
- [FIX] `RouterGroup.StaticFS()` leaked files. Now it closes them. [#2118](https://github.com/gin-gonic/gin/pull/2118)
- [FIX] `Context.Request.FormFile` leaked file. Now it closes it. [#2114](https://github.com/gin-gonic/gin/pull/2114)
- [FIX] Ignore walking on `form:"-"` mapping [#1943](https://github.com/gin-gonic/gin/pull/1943)
### Gin v1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) - [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
@ -15,7 +170,7 @@
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749) - [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252) - [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775) - [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260) - [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112) - [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238) - [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020) - [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
@ -56,7 +211,7 @@
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491) - [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin 1.3.0 ## Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383) - [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358) - [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
@ -78,7 +233,7 @@
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250) - [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460) - [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
### Gin 1.2.0 ## Gin 1.2.0
- [NEW] Switch from godeps to govendor - [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Add support for Let's Encrypt via gin-gonic/autotls
@ -101,25 +256,25 @@
- [FIX] Use X-Forwarded-For before X-Real-Ip - [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] time.Time binding (#904) - [FIX] time.Time binding (#904)
### Gin 1.1.4 ## Gin 1.1.4
- [NEW] Support google appengine for IsTerminal func - [NEW] Support google appengine for IsTerminal func
### Gin 1.1.3 ## Gin 1.1.3
- [FIX] Reverted Logger: skip ANSI color commands - [FIX] Reverted Logger: skip ANSI color commands
### Gin 1.1 ## Gin 1.1
- [NEW] Implement QueryArray and PostArray methods - [NEW] Implement QueryArray and PostArray methods
- [NEW] Refactor GetQuery and GetPostForm - [NEW] Refactor GetQuery and GetPostForm
- [NEW] Add contribution guide - [NEW] Add contribution guide
- [FIX] Corrected typos in README - [FIX] Corrected typos in README
- [FIX] Removed additional Iota - [FIX] Removed additional Iota
- [FIX] Changed imports to gopkg instead of github in README (#733) - [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty - [FIX] Logger: skip ANSI color commands if output is not a tty
### Gin 1.0rc2 (...) ## Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type. - [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing - [PERFORMANCE] Much faster 404 routing
@ -154,7 +309,7 @@
- [FIX] MIT license in every file - [FIX] MIT license in every file
### Gin 1.0rc1 (May 22, 2015) ## Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router - [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering - [PERFORMANCE] Faster JSON, XML and text rendering
@ -162,7 +317,7 @@
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations - [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
- [NEW] Built-in support for golang.org/x/net/context - [NEW] Built-in support for golang.org/x/net/context
- [NEW] Any(path, handler). Create a route that matches any path - [NEW] Any(path, handler). Create a route that matches any path
- [NEW] Refactored rendering pipeline (faster and static typeded) - [NEW] Refactored rendering pipeline (faster and static typed)
- [NEW] Refactored errors API - [NEW] Refactored errors API
- [NEW] IndentedJSON() prints pretty JSON - [NEW] IndentedJSON() prints pretty JSON
- [NEW] Added gin.DefaultWriter - [NEW] Added gin.DefaultWriter
@ -198,7 +353,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt) - [FIX] Better support for Google App Engine (using log instead of fmt)
### Gin 0.6 (Mar 9, 2015) ## Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data - [NEW] Support multipart/form-data
- [NEW] NoMethod handler - [NEW] NoMethod handler
@ -208,14 +363,14 @@
- [FIX] Improve color logger - [FIX] Improve color logger
### Gin 0.5 (Feb 7, 2015) ## Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation - [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip - [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding - [FIX] Fix unexported/ignored fields in binding
### Gin 0.4 (Aug 21, 2014) ## Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode - [NEW] Development mode
- [NEW] Unit tests - [NEW] Unit tests
@ -224,14 +379,14 @@
- [FIX] Improved documentation for model binding - [FIX] Improved documentation for model binding
### Gin 0.3 (Jul 18, 2014) ## Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call. - [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter() - [PERFORMANCE] Improve performance of NoRouter()
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults. - [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API - [NEW] Flexible rendering API
- [NEW] Add Context.File() - [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS - [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute() - [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged - [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving - [FIX] Adds HEAD method in Static file serving
@ -242,7 +397,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form - [FIX] Check application/x-www-form-urlencoded when parsing form
### Gin 0.2b (Jul 08, 2014) ## Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration - [NEW] Travis CI integration
- [NEW] Completely new logger - [NEW] Completely new logger
@ -254,14 +409,14 @@
- [NEW] New Bind() and BindWith() methods for parsing request body. - [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy() - [NEW] Add Content.Copy()
- [NEW] Add context.LastError() - [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method - [NEW] Add shortcut for OPTIONS HTTP method
- [FIX] Tons of README fixes - [FIX] Tons of README fixes
- [FIX] Header is written before body - [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit - [FIX] BasicAuth() and changes API a little bit
- [FIX] Recovery() middleware only prints panics - [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. - [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers - [FIX] Multiple http.WriteHeader() in NotFound handlers
- [FIX] Engine.Run() panics if http server can't be setted up - [FIX] Engine.Run() panics if http server can't be set up
- [FIX] Crash when route path doesn't start with '/' - [FIX] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative - [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String() - [FIX] Setting response headers before calling WriteHeader in context.String()

View File

@ -1,4 +1,4 @@
## Contributing ## Contributing
- With issues: - With issues:
- Use the search tool before opening a new issue. - Use the search tool before opening a new issue.
@ -8,6 +8,6 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integrations systems such as TravisCI. - It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.

View File

@ -1,20 +1,17 @@
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/) PACKAGES ?= $(shell $(GO) list ./...)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples) TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
TESTTAGS ?= ""
all: install
install: deps
govendor sync
.PHONY: test .PHONY: test
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ $(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \ cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \ if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \ rm tmp.out; \
@ -48,11 +45,6 @@ fmt-check:
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kardianos/govendor; \
fi
.PHONY: lint .PHONY: lint
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@ -76,5 +68,10 @@ misspell:
.PHONY: tools .PHONY: tools
tools: tools:
go install golang.org/x/lint/golint; \ @if [ $(GO_VERSION) -gt 15 ]; then \
go install github.com/client9/misspell/cmd/misspell; $(GO) install golang.org/x/lint/golint@latest; \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
elif [ $(GO_VERSION) -lt 16 ]; then \
$(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \
fi

582
README.md

File diff suppressed because it is too large Load Diff

19
auth.go
View File

@ -9,6 +9,8 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// AuthUserKey is the cookie name for user credential in basic auth. // AuthUserKey is the cookie name for user credential in basic auth.
@ -29,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if pair.value == authValue { if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true return pair.user, true
} }
} }
@ -69,8 +71,9 @@ func BasicAuth(accounts Accounts) HandlerFunc {
} }
func processAccounts(accounts Accounts) authPairs { func processAccounts(accounts Accounts) authPairs {
assert1(len(accounts) > 0, "Empty list of authorized credentials") length := len(accounts)
pairs := make(authPairs, 0, len(accounts)) assert1(length > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, length)
for user, password := range accounts { for user, password := range accounts {
assert1(user != "", "User can not be empty") assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password) value := authorizationHeader(user, password)
@ -84,13 +87,5 @@ func processAccounts(accounts Accounts) authPairs {
func authorizationHeader(user, password string) string { func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
}
func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
// Securely compare actual to itself to keep constant time, but always return false.
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
} }

View File

@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
} }
func TestBasicAuthSecureCompare(t *testing.T) {
assert.True(t, secureCompare("1234567890", "1234567890"))
assert.False(t, secureCompare("123456789", "1234567890"))
assert.False(t, secureCompare("12345678900", "1234567890"))
assert.False(t, secureCompare("1234567891", "1234567890"))
}
func TestBasicAuthSucceed(t *testing.T) { func TestBasicAuthSucceed(t *testing.T) {
accounts := Accounts{"admin": "password"} accounts := Accounts{"admin": "password"}
router := New() router := New()

View File

@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding package binding
import "net/http" import "net/http"
@ -46,10 +49,11 @@ type BindingUri interface {
// StructValidator is the minimal interface which needs to be implemented in // StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness // order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using // of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned. // If the received type is a slice|array, the validation should be performed travel on every element.
// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed. // If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned. // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned. // Otherwise nil must be returned.
@ -61,7 +65,7 @@ type StructValidator interface {
} }
// Validator is the default validator which implements the StructValidator // Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2 // interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood. // under the hood.
var Validator StructValidator = &defaultValidator{} var Validator StructValidator = &defaultValidator{}
@ -78,12 +82,13 @@ var (
MsgPack = msgpackBinding{} MsgPack = msgpackBinding{}
YAML = yamlBinding{} YAML = yamlBinding{}
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
// and the content type. // and the content type.
func Default(method, contentType string) Binding { func Default(method, contentType string) Binding {
if method == "GET" { if method == http.MethodGet {
return Form return Form
} }

View File

@ -1,72 +0,0 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

View File

@ -0,0 +1,58 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
}

View File

@ -0,0 +1,112 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build nomsgpack
// +build nomsgpack
package binding
import "net/http"
// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
)
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
}
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() interface{}
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEYAML:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}
func validate(obj interface{}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}

View File

@ -13,6 +13,7 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"reflect"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -21,11 +22,20 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample" "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
) )
type appkey struct {
Appkey string `json:"appkey" form:"appkey"`
}
type QueryTest struct {
Page int `json:"page" form:"page"`
Size int `json:"size" form:"size"`
appkey
}
type FooStruct struct { type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"` Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
} }
type FooBarStruct struct { type FooBarStruct struct {
@ -54,9 +64,20 @@ type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"` Foo interface{} `json:"foo" binding:"required"`
} }
type FooStructDisallowUnknownFields struct {
Foo interface{} `json:"foo" binding:"required"`
}
type FooBarStructForTimeType struct { type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
} }
type FooStructForTimeTypeNotFormat struct { type FooStructForTimeTypeNotFormat struct {
@ -142,9 +163,6 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, YAML, Default("POST", MIMEYAML)) assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML)) assert.Equal(t, YAML, Default("PUT", MIMEYAML))
} }
@ -163,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`) `{"foo": "bar"}`, `{"bar": "foo"}`)
} }
func TestBindingJSONSlice(t *testing.T) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
}
func TestBindingJSONUseNumber(t *testing.T) { func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t, testBodyBindingUseNumber(t,
JSON, "json", JSON, "json",
@ -177,6 +209,18 @@ func TestBindingJSONUseNumber2(t *testing.T) {
`{"foo": 123}`, `{"bar": "foo"}`) `{"foo": 123}`, `{"bar": "foo"}`)
} }
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
testBodyBindingDisallowUnknownFields(t, JSON,
"/", "/",
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
func TestBindingJSONStringMap(t *testing.T) {
testBodyBindingStringMap(t, JSON,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
}
func TestBindingForm(t *testing.T) { func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST", testFormBinding(t, "POST",
"/", "/", "/", "/",
@ -189,6 +233,18 @@ func TestBindingForm2(t *testing.T) {
"", "") "", "")
} }
func TestBindingFormEmbeddedStruct(t *testing.T) {
testFormBindingEmbeddedStruct(t, "POST",
"/", "/",
"page=1&size=2&appkey=test-appkey", "bar2=foo")
}
func TestBindingFormEmbeddedStruct2(t *testing.T) {
testFormBindingEmbeddedStruct(t, "GET",
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
"", "")
}
func TestBindingFormDefaultValue(t *testing.T) { func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST", testFormBindingDefaultValue(t, "POST",
"/", "/", "/", "/",
@ -204,7 +260,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) { func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST", testFormBindingForTime(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15&time_bar=", "bar2=foo") "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST", testFormBindingForTimeNotFormat(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15", "bar2=foo") "time_foo=2017-11-15", "bar2=foo")
@ -218,8 +277,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) { func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET", testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"", "") "", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET", testFormBindingForTimeNotFormat(t, "GET",
"/?time_foo=2017-11-15", "/?bar2=foo", "/?time_foo=2017-11-15", "/?bar2=foo",
"", "") "", "")
@ -295,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer") "", "", "StructPointer")
} }
func TestBindingFormStringMap(t *testing.T) {
testBodyBindingStringMap(t, Form,
"/", "",
`foo=bar&hello=world`, "")
// Should pick the last value
testBodyBindingStringMap(t, Form,
"/", "",
`foo=something&foo=bar&hello=world`, "")
}
func TestBindingFormStringSliceMap(t *testing.T) {
obj := make(map[string][]string)
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err := Form.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
target := map[string][]string{
"foo": {"something", "bar"},
"hello": {"world"},
}
assert.True(t, reflect.DeepEqual(obj, target))
objInvalid := make(map[string][]int)
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err = Form.Bind(req, &objInvalid)
assert.Error(t, err)
}
func TestBindingQuery(t *testing.T) { func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST", testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/", "/?foo=bar&bar=foo", "/",
@ -325,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "") "bool_foo=unused", "")
} }
func TestBindingQueryStringMap(t *testing.T) {
b := Query
obj := make(map[string]string)
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
obj = make(map[string]string)
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
err = b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "2", obj["foo"])
assert.Equal(t, "world", obj["hello"])
}
func TestBindingXML(t *testing.T) { func TestBindingXML(t *testing.T) {
testBodyBinding(t, testBodyBinding(t,
XML, "xml", XML, "xml",
@ -346,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`) `foo: bar`, `bar: foo`)
} }
func TestBindingYAMLStringMap(t *testing.T) {
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
testBodyBindingStringMap(t, YAML,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
}
func TestBindingYAMLFail(t *testing.T) { func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t, testBodyBindingFail(t,
YAML, "yaml", YAML, "yaml",
@ -396,7 +518,8 @@ func createFormFilesMultipartRequest(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file", "form.go") fw, err1 := mw.CreateFormFile("file", "form.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -420,7 +543,8 @@ func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go") fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -509,7 +633,8 @@ func TestBindingFormPostForMapFail(t *testing.T) {
func TestBindingFormFilesMultipart(t *testing.T) { func TestBindingFormFilesMultipart(t *testing.T) {
req := createFormFilesMultipartRequest(t) req := createFormFilesMultipartRequest(t)
var obj FooBarFileStruct var obj FooBarFileStruct
FormMultipart.Bind(req, &obj) err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
// file from os // file from os
f, _ := os.Open("form.go") f, _ := os.Open("form.go")
@ -585,26 +710,6 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:])) string(data), string(data[1:]))
} }
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func TestValidationFails(t *testing.T) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@ -623,9 +728,9 @@ func TestValidationDisabled(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestExistsSucceeds(t *testing.T) { func TestRequiredSucceeds(t *testing.T) {
type HogeStruct struct { type HogeStruct struct {
Hoge *int `json:"hoge" binding:"exists"` Hoge *int `json:"hoge" binding:"required"`
} }
var obj HogeStruct var obj HogeStruct
@ -634,9 +739,9 @@ func TestExistsSucceeds(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestExistsFails(t *testing.T) { func TestRequiredFails(t *testing.T) {
type HogeStruct struct { type HogeStruct struct {
Hoge *int `json:"foo" binding:"exists"` Hoge *int `json:"foo" binding:"required"`
} }
var obj HogeStruct var obj HogeStruct
@ -645,6 +750,31 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestHeaderBinding(t *testing.T) {
h := Header
assert.Equal(t, "header", h.Name())
type tHeader struct {
Limit int `header:"limit"`
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req.Header.Add("limit", "1000")
assert.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]interface{} `header:"fail"`
}
err := h.Bind(req, &failStruct{})
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) { func TestUriBinding(t *testing.T) {
b := Uri b := Uri
assert.Equal(t, "uri", b.Name()) assert.Equal(t, "uri", b.Name())
@ -688,6 +818,23 @@ func TestUriInnerBinding(t *testing.T) {
assert.Equal(t, tag.S.Age, expectedAge) assert.Equal(t, tag.S.Age, expectedAge)
} }
func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := QueryTest{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form b := Form
assert.Equal(t, "form", b.Name()) assert.Equal(t, "form", b.Name())
@ -785,6 +932,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
assert.Equal(t, "UTC", obj.TimeBar.Location().String()) assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
obj = FooBarStructForTimeType{} obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody) req = requestWithBody(method, badPath, badBody)
@ -792,6 +941,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Error(t, err) assert.Error(t, err)
} }
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.Error(t, err)
obj = FooStructForTimeTypeNotUnixFormat{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form b := Form
assert.Equal(t, "form", b.Name()) assert.Equal(t, "form", b.Name())
@ -1028,6 +1195,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err) assert.Error(t, err)
} }
func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
var obj1 []FooStruct
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj1)
assert.NoError(t, err)
var obj2 []FooStruct
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj2)
assert.Error(t, err)
}
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string)
req := requestWithBody("POST", path, body)
if b.Name() == "form" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
if badPath != "" && badBody != "" {
obj = make(map[string]string)
req = requestWithBody("POST", badPath, badBody)
err = b.Bind(req, &obj)
assert.Error(t, err)
}
objInt := make(map[string]int)
req = requestWithBody("POST", path, body)
err = b.Bind(req, &objInt)
assert.Error(t, err)
}
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
@ -1065,6 +1272,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.Error(t, err) assert.Error(t, err)
} }
func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
obj := FooStructDisallowUnknownFields{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStructDisallowUnknownFields{}
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
assert.Contains(t, err.Error(), "what")
}
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
@ -1121,23 +1347,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Error(t, err) assert.Error(t, err)
} }
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) { func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return return

View File

@ -5,10 +5,12 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"gopkg.in/go-playground/validator.v8" "github.com/go-playground/validator/v10"
) )
type defaultValidator struct { type defaultValidator struct {
@ -16,28 +18,72 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type sliceValidateError []error
// Error concatenates all error elements in sliceValidateError into a single string separated by \n.
func (err sliceValidateError) Error() string {
n := len(err)
switch n {
case 0:
return ""
default:
var b strings.Builder
if err[0] != nil {
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
}
if n > 1 {
for i := 1; i < n; i++ {
if err[i] != nil {
b.WriteString("\n")
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
}
}
}
return b.String()
}
}
var _ StructValidator = &defaultValidator{} var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
valueType := value.Kind() switch value.Kind() {
if valueType == reflect.Ptr { case reflect.Ptr:
valueType = value.Elem().Kind() return v.ValidateStruct(value.Elem().Interface())
} case reflect.Struct:
if valueType == reflect.Struct { return v.validateStruct(obj)
v.lazyinit() case reflect.Slice, reflect.Array:
if err := v.validate.Struct(obj); err != nil { count := value.Len()
return err validateRet := make(sliceValidateError, 0)
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}
} }
if len(validateRet) == 0 {
return nil
}
return validateRet
default:
return nil
} }
return nil }
// validateStruct receives struct type
func (v *defaultValidator) validateStruct(obj interface{}) error {
v.lazyinit()
return v.validate.Struct(obj)
} }
// Engine returns the underlying validator engine which powers the default // Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations // Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info - // or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8 // https://pkg.go.dev/github.com/go-playground/validator/v10
func (v *defaultValidator) Engine() interface{} { func (v *defaultValidator) Engine() interface{} {
v.lazyinit() v.lazyinit()
return v.validate return v.validate
@ -45,7 +91,7 @@ func (v *defaultValidator) Engine() interface{} {
func (v *defaultValidator) lazyinit() { func (v *defaultValidator) lazyinit() {
v.once.Do(func() { v.once.Do(func() {
config := &validator.Config{TagName: "binding"} v.validate = validator.New()
v.validate = validator.New(config) v.validate.SetTagName("binding")
}) })
} }

View File

@ -0,0 +1,20 @@
package binding
import (
"errors"
"strconv"
"testing"
)
func BenchmarkSliceValidateError(b *testing.B) {
const size int = 100
for i := 0; i < b.N; i++ {
e := make(sliceValidateError, size)
for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j))
}
if len(e.Error()) == 0 {
b.Errorf("error")
}
}
}

View File

@ -0,0 +1,88 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"testing"
)
func TestSliceValidateError(t *testing.T) {
tests := []struct {
name string
err sliceValidateError
want string
}{
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
{"has zero elements", sliceValidateError{}, ""},
{"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements",
sliceValidateError{
errors.New("first error"),
errors.New("second error"),
},
"[0]: first error\n[1]: second error",
},
{"has many elements",
sliceValidateError{
errors.New("first error"),
errors.New("second error"),
nil,
nil,
nil,
errors.New("last error"),
},
"[0]: first error\n[1]: second error\n[5]: last error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestDefaultValidator(t *testing.T) {
type exampleStruct struct {
A string `binding:"max=8"`
B int `binding:"gt=0"`
}
tests := []struct {
name string
v *defaultValidator
obj interface{}
wantErr bool
}{
{"validate nil obj", &defaultValidator{}, nil, false},
{"validate int obj", &defaultValidator{}, 3, false},
{"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
{"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
{"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
{"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
{"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
{"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
{"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
{"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -5,12 +5,10 @@
package binding package binding
import ( import (
"mime/multipart"
"net/http" "net/http"
"reflect"
) )
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 << 20
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{} type formPostBinding struct{}
@ -24,10 +22,8 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil && err != http.ErrNotMultipart {
if err != http.ErrNotMultipart { return err
return err
}
} }
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
@ -63,27 +59,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj) return validate(obj)
} }
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if value.Type() == multipartFileHeaderStructType {
_, file, err := (*http.Request)(r).FormFile(key)
if err != nil {
return false, err
}
if file != nil {
value.Set(reflect.ValueOf(*file))
return true, nil
}
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}

View File

@ -12,10 +12,19 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
var errUnknownType = errors.New("Unknown type") var (
errUnknownType = errors.New("unknown type")
// ErrConvertMapStringSlice can not covert to map[string][]string
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
// ErrConvertToMapString can not convert to map[string]string
ErrConvertToMapString = errors.New("can not convert to map of strings")
)
func mapUri(ptr interface{}, m map[string][]string) error { func mapUri(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri") return mapFormByTag(ptr, m, "uri")
@ -25,9 +34,28 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return mapFormByTag(ptr, form, "form") return mapFormByTag(ptr, form, "form")
} }
func MapFormWithTag(ptr interface{}, form map[string][]string, tag string) error {
return mapFormByTag(ptr, form, tag)
}
var emptyField = reflect.StructField{} var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
// Check if ptr is a map
ptrVal := reflect.ValueOf(ptr)
var pointed interface{}
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface()
}
if ptrVal.Kind() == reflect.Map &&
ptrVal.Type().Key().Kind() == reflect.String {
if pointed != nil {
ptr = pointed
}
return setFormMap(ptr, form)
}
return mappingByPtr(ptr, formSource(form), tag) return mappingByPtr(ptr, formSource(form), tag)
} }
@ -51,6 +79,10 @@ func mappingByPtr(ptr interface{}, setter setter, tag string) error {
} }
func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
if field.Tag.Get(tag) == "-" { // just ignoring this field
return false, nil
}
var vKind = value.Kind() var vKind = value.Kind()
if vKind == reflect.Ptr { if vKind == reflect.Ptr {
@ -70,12 +102,14 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return isSetted, nil return isSetted, nil
} }
ok, err := tryToSetValue(value, field, setter, tag) if vKind != reflect.Struct || !field.Anonymous {
if err != nil { ok, err := tryToSetValue(value, field, setter, tag)
return false, err if err != nil {
} return false, err
if ok { }
return true, nil if ok {
return true, nil
}
} }
if vKind == reflect.Struct { if vKind == reflect.Struct {
@ -83,10 +117,11 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
var isSetted bool var isSetted bool
for i := 0; i < value.NumField(); i++ { for i := 0; i < value.NumField(); i++ {
if !value.Field(i).CanSet() { sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue continue
} }
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -109,9 +144,6 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
tagValue = field.Tag.Get(tag) tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",") tagValue, opts := head(tagValue, ",")
if tagValue == "-" { // just ignoring this field
return false, nil
}
if tagValue == "" { // default value is FieldName if tagValue == "" { // default value is FieldName
tagValue = field.Name tagValue = field.Name
} }
@ -123,9 +155,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
for len(opts) > 0 { for len(opts) > 0 {
opt, opts = head(opts, ",") opt, opts = head(opts, ",")
k, v := head(opt, "=") if k, v := head(opt, "="); k == "default" {
switch k {
case "default":
setOpt.isDefaultExists = true setOpt.isDefaultExists = true
setOpt.defaultValue = v setOpt.defaultValue = v
} }
@ -206,9 +236,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case time.Time: case time.Time:
return setTimeField(val, field, value) return setTimeField(val, field, value)
} }
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
default: default:
return errUnknownType return errUnknownType
} }
@ -265,6 +295,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339 timeFormat = time.RFC3339
} }
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" { if val == "" {
value.Set(reflect.ValueOf(time.Time{})) value.Set(reflect.ValueOf(time.Time{}))
return nil return nil
@ -328,3 +376,29 @@ func head(str, sep string) (head string, tail string) {
} }
return str[:idx], str[idx+len(sep):] return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr interface{}, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string)
if !ok {
return ErrConvertMapStringSlice
}
for k, v := range form {
ptrMap[k] = v
}
return nil
}
ptrMap, ok := ptr.(map[string]string)
if !ok {
return ErrConvertToMapString
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
return nil
}

View File

@ -32,7 +32,10 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) { func BenchmarkMapFormFull(b *testing.B) {
var s structFull var s structFull
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()
@ -52,7 +55,10 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) { func BenchmarkMapFormName(b *testing.B) {
var s structName var s structName
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()

View File

@ -145,6 +145,15 @@ func TestMappingForm(t *testing.T) {
assert.Equal(t, int(6), s.F) assert.Equal(t, int(6), s.F)
} }
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingTime(t *testing.T) { func TestMappingTime(t *testing.T) {
var s struct { var s struct {
Time time.Time Time time.Time
@ -190,7 +199,7 @@ func TestMappingTime(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestMapiingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
var s struct { var s struct {
D time.Duration D time.Duration
} }
@ -269,3 +278,13 @@ func TestMappingMapField(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M) assert.Equal(t, map[string]int{"one": 1}, s.M)
} }
func TestMappingIgnoredCircularRef(t *testing.T) {
type S struct {
S *S `form:"-"`
}
var s S
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
}

34
binding/header.go Normal file
View File

@ -0,0 +1,34 @@
package binding
import (
"net/http"
"net/textproto"
"reflect"
)
type headerBinding struct{}
func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
return validate(obj)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -6,7 +6,7 @@ package binding
import ( import (
"bytes" "bytes"
"fmt" "errors"
"io" "io"
"net/http" "net/http"
@ -18,6 +18,12 @@ import (
// interface{} as a Number instead of as a float64. // interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
// return an error when the destination is a struct and the input contains object
// keys which do not match any non-ignored, exported fields in the destination.
var EnableDecoderDisallowUnknownFields = false
type jsonBinding struct{} type jsonBinding struct{}
func (jsonBinding) Name() string { func (jsonBinding) Name() string {
@ -26,7 +32,7 @@ func (jsonBinding) Name() string {
func (jsonBinding) Bind(req *http.Request, obj interface{}) error { func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil { if req == nil || req.Body == nil {
return fmt.Errorf("invalid request") return errors.New("invalid request")
} }
return decodeJSON(req.Body, obj) return decodeJSON(req.Body, obj)
} }
@ -40,6 +46,9 @@ func decodeJSON(r io.Reader, obj interface{}) error {
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }
if EnableDecoderDisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err
} }

30
binding/json_test.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONBindingBindBody(t *testing.T) {
var s struct {
Foo string `json:"foo"`
}
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func TestJSONBindingBindBodyMap(t *testing.T) {
s := make(map[string]string)
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
require.NoError(t, err)
assert.Len(t, s, 2)
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}

View File

@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding package binding
import ( import (

35
binding/msgpack_test.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj interface{}) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -0,0 +1,74 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"mime/multipart"
"net/http"
"reflect"
)
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
// ErrMultiFileHeader multipart.FileHeader invalid
ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader")
// ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid
ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*multipart.FileHeader")
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (bool, error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, ErrMultiFileHeader
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) {
return false, ErrMultiFileHeaderLenInvalid
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
}
}
return true, nil
}

View File

@ -0,0 +1,138 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io/ioutil"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
var s struct {
FileValue multipart.FileHeader `form:"file"`
FilePtr *multipart.FileHeader `form:"file"`
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [1]multipart.FileHeader `form:"file"`
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
}
file := testFile{"file", "file1", []byte("hello")}
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
assert.Len(t, s.SliceValues, 1)
assertMultipartFileHeader(t, &s.SliceValues[0], file)
assert.Len(t, s.SlicePtrs, 1)
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
}
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
var s struct {
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [2]multipart.FileHeader `form:"file"`
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
}
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
assert.Len(t, s.ArrayValues, len(files))
assert.Len(t, s.ArrayPtrs, len(files))
for i, file := range files {
assertMultipartFileHeader(t, &s.SliceValues[i], file)
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
}
}
func TestFormMultipartBindingBindError(t *testing.T) {
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
for _, tt := range []struct {
name string
s interface{}
}{
{"wrong type", &struct {
Files int `form:"file"`
}{}},
{"wrong array size", &struct {
Files [1]*multipart.FileHeader `form:"file"`
}{}},
{"wrong slice type", &struct {
Files []int `form:"file"`
}{}},
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
}
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open()
assert.NoError(t, err)
body, err := ioutil.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}

View File

@ -6,12 +6,11 @@ package binding
import ( import (
"bytes" "bytes"
"reflect"
"testing" "testing"
"time" "time"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
) )
type testInterface interface { type testInterface interface {
@ -200,15 +199,8 @@ type structCustomValidation struct {
Integer int `binding:"notone"` Integer int `binding:"notone"`
} }
// notOne is a custom validator meant to be used with `validator.v8` library. func notOne(f1 validator.FieldLevel) bool {
// The method signature for `v9` is significantly different and this function if val, ok := f1.Field().Interface().(int); ok {
// would need to be changed for tests to pass after upgrade.
// See https://github.com/gin-gonic/gin/pull/1015.
func notOne(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if val, ok := field.Interface().(int); ok {
return val != 1 return val != 1
} }
return false return false

25
binding/xml_test.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestXMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `xml:"foo"`
}
xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`
err := xmlBinding{}.BindBody([]byte(xmlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

21
binding/yaml_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"math" "math"
"mime/multipart" "mime/multipart"
"net" "net"
@ -16,6 +17,7 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/gin-contrib/sse" "github.com/gin-contrib/sse"
@ -33,10 +35,13 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
const abortIndex int8 = math.MaxInt8 / 2 // BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
@ -48,8 +53,13 @@ type Context struct {
Params Params Params Params
handlers HandlersChain handlers HandlersChain
index int8 index int8
fullPath string
engine *Engine engine *Engine
params *Params
// This mutex protect Keys map
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request. // Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{} Keys map[string]interface{}
@ -59,6 +69,17 @@ type Context struct {
// Accepted defines a list of manually accepted formats for content negotiation. // Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
} }
/************************************/ /************************************/
@ -67,18 +88,28 @@ type Context struct {
func (c *Context) reset() { func (c *Context) reset() {
c.Writer = &c.writermem c.Writer = &c.writermem
c.Params = c.Params[0:0] c.Params = c.Params[:0]
c.handlers = nil c.handlers = nil
c.index = -1 c.index = -1
c.fullPath = ""
c.Keys = nil c.Keys = nil
c.Errors = c.Errors[0:0] c.Errors = c.Errors[:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil
c.formCache = nil
*c.params = (*c.params)[:0]
} }
// Copy returns a copy of the current context that can be safely used outside the request's scope. // Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine. // This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context { func (c *Context) Copy() *Context {
var cp = *c cp := Context{
writermem: c.writermem,
Request: c.Request,
Params: c.Params,
engine: c.engine,
}
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
@ -87,6 +118,9 @@ func (c *Context) Copy() *Context {
for k, v := range c.Keys { for k, v := range c.Keys {
cp.Keys[k] = v cp.Keys[k] = v
} }
paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cp.Params = paramCopy
return &cp return &cp
} }
@ -111,6 +145,15 @@ func (c *Context) Handler() HandlerFunc {
return c.handlers.Last() return c.handlers.Last()
} }
// FullPath returns a matched route full path. For not found routes
// returns an empty string.
// router.GET("/user/:id", func(c *gin.Context) {
// c.FullPath() == "/user/:id" // true
// })
func (c *Context) FullPath() string {
return c.fullPath
}
/************************************/ /************************************/
/*********** FLOW CONTROL ***********/ /*********** FLOW CONTROL ***********/
/************************************/ /************************************/
@ -196,16 +239,21 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context. // Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) { func (c *Context) Set(key string, value interface{}) {
c.mu.Lock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]interface{}) c.Keys = make(map[string]interface{})
} }
c.Keys[key] = value c.Keys[key] = value
c.mu.Unlock()
} }
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { func (c *Context) Get(key string) (value interface{}, exists bool) {
c.mu.RLock()
value, exists = c.Keys[key] value, exists = c.Keys[key]
c.mu.RUnlock()
return return
} }
@ -249,6 +297,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
return return
} }
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) {
if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
}
// GetUint64 returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint64(key string) (ui64 uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
}
return
}
// GetFloat64 returns the value associated with the key as a float64. // GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) { func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil { if val, ok := c.Get(key); ok && val != nil {
@ -368,10 +432,21 @@ func (c *Context) QueryArray(key string) []string {
return values return values
} }
func (c *Context) initQueryCache() {
if c.queryCache == nil {
if c.Request != nil {
c.queryCache = c.Request.URL.Query()
} else {
c.queryCache = url.Values{}
}
}
}
// GetQueryArray returns a slice of strings for a given query key, plus // GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) { func (c *Context) GetQueryArray(key string) ([]string, bool) {
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 { c.initQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true return values, true
} }
return []string{}, false return []string{}, false
@ -386,7 +461,8 @@ func (c *Context) QueryMap(key string) map[string]string {
// GetQueryMap returns a map for a given query key, plus a boolean value // GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) { func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key) c.initQueryCache()
return c.get(c.queryCache, key)
} }
// PostForm returns the specified key from a POST urlencoded form or multipart form // PostForm returns the specified key from a POST urlencoded form or multipart form
@ -427,16 +503,24 @@ func (c *Context) PostFormArray(key string) []string {
return values return values
} }
func (c *Context) initFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
c.formCache = req.PostForm
}
}
// GetPostFormArray returns a slice of strings for a given form key, plus // GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request c.initFormCache()
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { if values := c.formCache[key]; len(values) > 0 {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
return []string{}, false return []string{}, false
@ -451,13 +535,8 @@ func (c *Context) PostFormMap(key string) map[string]string {
// GetPostFormMap returns a map for a given form key, plus a boolean value // GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request c.initFormCache()
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { return c.get(c.formCache, key)
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
return c.get(req.PostForm, key)
} }
// get is an internal method and returns a map which satisfy conditions. // get is an internal method and returns a map which satisfy conditions.
@ -482,7 +561,11 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
return nil, err return nil, err
} }
} }
_, fh, err := c.Request.FormFile(name) f, fh, err := c.Request.FormFile(name)
if err != nil {
return nil, err
}
f.Close()
return fh, err return fh, err
} }
@ -543,6 +626,11 @@ func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML) return c.MustBindWith(obj, binding.YAML)
} }
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj interface{}) error {
return c.MustBindWith(obj, binding.Header)
}
// BindUri binds the passed struct pointer using binding.Uri. // BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs. // It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error { func (c *Context) BindUri(obj interface{}) error {
@ -597,6 +685,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML) return c.ShouldBindWith(obj, binding.YAML)
} }
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error { func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string) m := make(map[string][]string)
@ -634,32 +727,94 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj) return bb.BindBody(body, obj)
} }
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP.
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. // If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,
// the remote IP (coming form Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { // Check if we're running on a trusted platform
clientIP := c.requestHeader("X-Forwarded-For") switch c.engine.TrustedPlatform {
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0]) case PlatformGoogleAppEngine:
if clientIP == "" { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) return addr
} }
if clientIP != "" { case PlatformCloudflare:
return clientIP if addr := c.requestHeader("CF-Connecting-IP"); addr != "" {
return addr
} }
} }
// Legacy "AppEngine" flag
if c.engine.AppEngine { if c.engine.AppEngine {
log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`)
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr return addr
} }
} }
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { remoteIP, trusted := c.RemoteIP()
return ip if remoteIP == nil {
return ""
} }
return "" if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := validateHeader(c.requestHeader(headerName))
if valid {
return ip
}
}
}
return remoteIP.String()
}
// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined in Engine.TrustedProxies
func (c *Context) RemoteIP() (net.IP, bool) {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return nil, false
}
remoteIP := net.ParseIP(ip)
if remoteIP == nil {
return nil, false
}
if c.engine.trustedCIDRs != nil {
for _, cidr := range c.engine.trustedCIDRs {
if cidr.Contains(remoteIP) {
return remoteIP, true
}
}
}
return remoteIP, false
}
func validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i, ipStr := range items {
ipStr = strings.TrimSpace(ipStr)
ip := net.ParseIP(ipStr)
if ip == nil {
return "", false
}
// We need to return the first IP in the list, but,
// we should not early return since we need to validate that
// the rest of the header is syntactically valid
if i == 0 {
clientIP = ipStr
valid = true
}
}
return
} }
// ContentType returns the Content-Type header of the request. // ContentType returns the Content-Type header of the request.
@ -671,7 +826,7 @@ func (c *Context) ContentType() string {
// handshake is being initiated by the client. // handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool { func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") && if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" { strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
return true return true
} }
return false return false
@ -700,7 +855,7 @@ func bodyAllowedForStatus(status int) bool {
// Status sets the HTTP response code. // Status sets the HTTP response code.
func (c *Context) Status(code int) { func (c *Context) Status(code int) {
c.writermem.WriteHeader(code) c.Writer.WriteHeader(code)
} }
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value). // Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
@ -724,6 +879,11 @@ func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body) return ioutil.ReadAll(c.Request.Body)
} }
// SetSameSite with cookie
func (c *Context) SetSameSite(samesite http.SameSite) {
c.sameSite = samesite
}
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers. // SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be // The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped. // silently dropped.
@ -737,6 +897,7 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
MaxAge: maxAge, MaxAge: maxAge,
Path: path, Path: path,
Domain: domain, Domain: domain,
SameSite: c.sameSite,
Secure: secure, Secure: secure,
HttpOnly: httpOnly, HttpOnly: httpOnly,
}) })
@ -790,11 +951,11 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// Default prepends "while(1)," to response body if the given struct is array values. // Default prepends "while(1)," to response body if the given struct is array values.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) SecureJSON(code int, obj interface{}) { func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj}) c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
} }
// JSONP serializes the given struct as JSON into the response body. // JSONP serializes the given struct as JSON into the response body.
// It add padding to response body to request data from a server residing in a different domain than the client. // It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript". // It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) { func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "") callback := c.DefaultQuery("callback", "")
@ -871,15 +1032,26 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
}) })
} }
// File writes the specified file into the body stream in a efficient way. // File writes the specified file into the body stream in an efficient way.
func (c *Context) File(filepath string) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
defer func(old string) {
c.Request.URL.Path = old
}(c.Request.URL.Path)
c.Request.URL.Path = filepath
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
}
// FileAttachment writes the specified file into the body stream in an efficient way // FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
@ -921,6 +1093,7 @@ type Negotiate struct {
HTMLData interface{} HTMLData interface{}
JSONData interface{} JSONData interface{}
XMLData interface{} XMLData interface{}
YAMLData interface{}
Data interface{} Data interface{}
} }
@ -939,6 +1112,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML:
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
} }
@ -955,20 +1132,20 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return offered[0] return offered[0]
} }
for _, accepted := range c.Accepted { for _, accepted := range c.Accepted {
for _, offert := range offered { for _, offer := range offered {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune // therefore we can just iterate over the string without casting it into []rune
i := 0 i := 0
for ; i < len(accepted); i++ { for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' { if accepted[i] == '*' || offer[i] == '*' {
return offert return offer
} }
if accepted[i] != offert[i] { if accepted[i] != offer[i] {
break break
} }
} }
if i == len(accepted) { if i == len(accepted) {
return offert return offer
} }
} }
} }
@ -984,28 +1161,28 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// Deadline returns the time when work done on behalf of this context // Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
return if c.Request == nil || c.Request.Context() == nil {
return
}
return c.Request.Context().Deadline()
} }
// Done returns a channel that's closed when work done on behalf of this // Done returns nil (chan which will wait forever) when c.Request has no Context.
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
return nil if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Done()
} }
// Err returns a non-nil error value after Done is closed, // Err returns nil when c.Request has no Context.
// successive calls to Err return the same error.
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
func (c *Context) Err() error { func (c *Context) Err() error {
return nil if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Err()
} }
// Value returns the value associated with this context for key, or nil // Value returns the value associated with this context for key, or nil
@ -1016,8 +1193,12 @@ func (c *Context) Value(key interface{}) interface{} {
return c.Request return c.Request
} }
if keyAsString, ok := key.(string); ok { if keyAsString, ok := key.(string); ok {
val, _ := c.Get(keyAsString) if val, exists := c.Get(keyAsString); exists {
return val return val
}
} }
return nil if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Value(key)
} }

View File

@ -1,11 +1,12 @@
// +build appengine
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
package gin package gin
func init() { func init() {
defaultAppEngine = true defaultPlatform = PlatformGoogleAppEngine
} }

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@ -13,8 +14,10 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"reflect" "reflect"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@ -22,7 +25,6 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
@ -259,6 +261,18 @@ func TestContextGetInt64(t *testing.T) {
assert.Equal(t, int64(42424242424242), c.GetInt64("int64")) assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
} }
func TestContextGetUint(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint", uint(1))
assert.Equal(t, uint(1), c.GetUint("uint"))
}
func TestContextGetUint64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint64", uint64(18446744073709551615))
assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
}
func TestContextGetFloat64(t *testing.T) { func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2) c.Set("float64", 4.2)
@ -408,6 +422,21 @@ func TestContextQuery(t *testing.T) {
assert.Empty(t, c.PostForm("foo")) assert.Empty(t, c.PostForm("foo"))
} }
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
assert.NotPanics(t, func() {
value, ok := c.GetQuery("NoKey")
assert.False(t, ok)
assert.Empty(t, value)
})
assert.NotPanics(t, func() {
assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
})
assert.NotPanics(t, func() {
assert.Empty(t, c.Query("NoKey"))
})
}
func TestContextQueryAndPostForm(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
@ -600,14 +629,16 @@ func TestContextPostFormMultipart(t *testing.T) {
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextSetCookiePathEmpty(t *testing.T) { func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "", "localhost", true, true) c.SetCookie("user", "gin", 1, "", "localhost", true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextGetCookie(t *testing.T) { func TestContextGetCookie(t *testing.T) {
@ -674,7 +705,7 @@ func TestContextRenderJSONP(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"}) c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String()) assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -936,7 +967,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytestring`
// with specified MIME type // with specified MIME type
func TestContextRenderData(t *testing.T) { func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -987,7 +1018,24 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
}
func TestContextRenderFileFromFS(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/some/path", nil)
c.FileFromFS("./gin.go", Dir(".", false))
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
} }
func TestContextRenderAttachment(t *testing.T) { func TestContextRenderAttachment(t *testing.T) {
@ -1000,7 +1048,7 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition")) assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
} }
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
@ -1112,7 +1160,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEJSON, MIMEXML}, Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
@ -1127,7 +1175,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON}, Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
@ -1238,7 +1286,7 @@ func TestContextIsAborted(t *testing.T) {
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytestring`
// with specified MIME type // with specified MIME type
func TestContextAbortWithStatus(t *testing.T) { func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -1281,7 +1329,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body) _, err := buf.ReadFrom(w.Body)
assert.NoError(t, err) assert.NoError(t, err)
jsonStringBody := buf.String() jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody) assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
} }
func TestContextError(t *testing.T) { func TestContextError(t *testing.T) {
@ -1347,12 +1395,11 @@ func TestContextAbortWithError(t *testing.T) {
func TestContextClientIP(t *testing.T) { func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") // Legacy tests (validating that the defaults don't break the
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") // (insecure!) old behaviour)
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, "20.20.20.20", c.ClientIP()) assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
@ -1363,7 +1410,7 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP") c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP()) assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr") c.Request.Header.Del("X-Appengine-Remote-Addr")
@ -1372,6 +1419,91 @@ func TestContextClientIP(t *testing.T) {
// no port // no port
c.Request.RemoteAddr = "50.50.50.50" c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP()) assert.Empty(t, c.ClientIP())
// Tests exercising the TrustedProxies functionality
resetContextForClientIPTests(c)
// No trusted proxies
_ = c.engine.SetTrustedProxies([]string{})
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Last proxy is trusted, but the RemoteAddr is not
_ = c.engine.SetTrustedProxies([]string{"30.30.30.30"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// All steps are trusted
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30", "20.20.20.20"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use CIDR
_ = c.engine.SetTrustedProxies([]string{"40.40.25.25/16", "30.30.30.30"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use hostname that resolves to all the proxies
_ = c.engine.SetTrustedProxies([]string{"foo"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
_ = c.engine.SetTrustedProxies([]string{"bar"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", " blah ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Result from LookupHost has non-IP element. This should never
// happen, but we should test it to make sure we handle it
// gracefully.
_ = c.engine.SetTrustedProxies([]string{"baz"})
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Del("X-Forwarded-For")
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.engine.RemoteIPHeaders = []string{}
c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
// Test the legacy flag
c.engine.TrustedPlatform = ""
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.engine.AppEngine = false
c.engine.TrustedPlatform = PlatformGoogleAppEngine
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = PlatformCloudflare
assert.Equal(t, "60.60.60.60", c.ClientIP())
c.Request.Header.Del("CF-Connecting-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = ""
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
}
func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.TrustedPlatform = ""
c.engine.AppEngine = false
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -1434,6 +1566,28 @@ func TestContextBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.BindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithQuery(t *testing.T) { func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1541,6 +1695,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextShouldBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.ShouldBindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) { func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1753,6 +1929,23 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition"))
} }
func TestContextRenderDataFromReaderNoHeaders(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
body := "#!PNG some raw data"
reader := strings.NewReader(body)
contentLength := int64(len(body))
contentType := "image/png"
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, nil)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, contentType, w.Header().Get("Content-Type"))
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length"))
}
type TestResponseRecorder struct { type TestResponseRecorder struct {
*httptest.ResponseRecorder *httptest.ResponseRecorder
closeChannel chan bool closeChannel chan bool
@ -1821,3 +2014,141 @@ func TestContextResetInHandler(t *testing.T) {
c.Next() c.Next()
}) })
} }
func TestRaceParamsContextCopy(t *testing.T) {
DefaultWriter = os.Stdout
router := Default()
nameGroup := router.Group("/:name")
var wg sync.WaitGroup
wg.Add(2)
{
nameGroup.GET("/api", func(c *Context) {
go func(c *Context, param string) {
defer wg.Done()
// First assert must be executed after the second request
time.Sleep(50 * time.Millisecond)
assert.Equal(t, c.Param("name"), param)
}(c.Copy(), c.Param("name"))
})
}
performRequest(router, "GET", "/name1/api")
performRequest(router, "GET", "/name2/api")
wg.Wait()
}
func TestContextWithKeysMutex(t *testing.T) {
c := &Context{}
c.Set("foo", "bar")
value, err := c.Get("foo")
assert.Equal(t, "bar", value)
assert.True(t, err)
value, err = c.Get("foo2")
assert.Nil(t, value)
assert.False(t, err)
}
func TestRemoteIPFail(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.RemoteAddr = "[:::]:80"
ip, trust := c.RemoteIP()
assert.Nil(t, ip)
assert.False(t, trust)
}
func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) {
c := &Context{}
deadline, ok := c.Deadline()
assert.Zero(t, deadline)
assert.False(t, ok)
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
d := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
c2.Request = c2.Request.WithContext(ctx)
deadline, ok = c2.Deadline()
assert.Equal(t, d, deadline)
assert.True(t, ok)
}
func TestContextWithFallbackDoneFromRequestContext(t *testing.T) {
c := &Context{}
assert.Nil(t, c.Done())
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.NotNil(t, <-c2.Done())
}
func TestContextWithFallbackErrFromRequestContext(t *testing.T) {
c := &Context{}
assert.Nil(t, c.Err())
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.EqualError(t, c2.Err(), context.Canceled.Error())
}
func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
tests := []struct {
name string
getContextAndKey func() (*Context, interface{})
value interface{}
}{
{
name: "c with struct context key",
getContextAndKey: func() (*Context, interface{}) {
var key struct{}
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "value"))
return c, key
},
value: "value",
},
{
name: "c with string context key",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), "key", "value"))
return c, "key"
},
value: "value",
},
{
name: "c with nil http.Request",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
return c, "key"
},
value: nil,
},
{
name: "c with nil http.Request.Context()",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
return c, "key"
},
value: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, key := tt.getContextAndKey()
assert.Equal(t, tt.value, c.Value(key))
})
}
}

View File

@ -5,16 +5,14 @@
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"os"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
) )
const ginSupportMinGoVer = 8 const ginSupportMinGoVer = 13
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -39,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
func debugPrintLoadTemplate(tmpl *template.Template) { func debugPrintLoadTemplate(tmpl *template.Template) {
if IsDebugging() { if IsDebugging() {
var buf bytes.Buffer var buf strings.Builder
for _, tmpl := range tmpl.Templates() { for _, tmpl := range tmpl.Templates() {
buf.WriteString("\t- ") buf.WriteString("\t- ")
buf.WriteString(tmpl.Name()) buf.WriteString(tmpl.Name())
@ -54,7 +52,7 @@ func debugPrint(format string, values ...interface{}) {
if !strings.HasSuffix(format, "\n") { if !strings.HasSuffix(format, "\n") {
format += "\n" format += "\n"
} }
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...) fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
} }
@ -69,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.13+.
`) `)
} }
@ -97,7 +95,7 @@ at initialization. ie. before any route is registered or the router is listening
} }
func debugPrintError(err error) { func debugPrintError(err error) {
if err != nil { if err != nil && IsDebugging() {
debugPrint("[ERROR] %v\n", err) fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
} }
} }

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"html/template" "html/template"
"io" "io"
"log" "log"
@ -64,6 +65,18 @@ func TestDebugPrintRoutes(t *testing.T) {
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
} }
func TestDebugPrintRouteFunc(t *testing.T) {
DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintLoadTemplate(t *testing.T) { func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
@ -91,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.13+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }
@ -111,15 +124,15 @@ func captureOutput(t *testing.T, f func()) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }
stdout := os.Stdout defaultWriter := DefaultWriter
stderr := os.Stderr defaultErrorWriter := DefaultErrorWriter
defer func() { defer func() {
os.Stdout = stdout DefaultWriter = defaultWriter
os.Stderr = stderr DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
}() }()
os.Stdout = writer DefaultWriter = writer
os.Stderr = writer DefaultErrorWriter = writer
log.SetOutput(writer) log.SetOutput(writer)
out := make(chan string) out := make(chan string)
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)

View File

@ -5,9 +5,9 @@
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -55,7 +55,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
// JSON creates a properly formatted JSON // JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} { func (msg *Error) JSON() interface{} {
json := H{} jsonData := H{}
if msg.Meta != nil { if msg.Meta != nil {
value := reflect.ValueOf(msg.Meta) value := reflect.ValueOf(msg.Meta)
switch value.Kind() { switch value.Kind() {
@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
return msg.Meta return msg.Meta
case reflect.Map: case reflect.Map:
for _, key := range value.MapKeys() { for _, key := range value.MapKeys() {
json[key.String()] = value.MapIndex(key).Interface() jsonData[key.String()] = value.MapIndex(key).Interface()
} }
default: default:
json["meta"] = msg.Meta jsonData["meta"] = msg.Meta
} }
} }
if _, ok := json["error"]; !ok { if _, ok := jsonData["error"]; !ok {
json["error"] = msg.Error() jsonData["error"] = msg.Error()
} }
return json return jsonData
} }
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 return (msg.Type & flags) > 0
} }
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error {
return msg.Err
}
// ByType returns a readonly copy filtered the byte. // ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs { func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@ -135,17 +140,17 @@ func (a errorMsgs) Errors() []string {
} }
func (a errorMsgs) JSON() interface{} { func (a errorMsgs) JSON() interface{} {
switch len(a) { switch length := len(a); length {
case 0: case 0:
return nil return nil
case 1: case 1:
return a.Last().JSON() return a.Last().JSON()
default: default:
json := make([]interface{}, len(a)) jsonData := make([]interface{}, length)
for i, err := range a { for i, err := range a {
json[i] = err.JSON() jsonData[i] = err.JSON()
} }
return json return jsonData
} }
} }
@ -158,7 +163,7 @@ func (a errorMsgs) String() string {
if len(a) == 0 { if len(a) == 0 {
return "" return ""
} }
var buffer bytes.Buffer var buffer strings.Builder
for i, msg := range a { for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil { if msg.Meta != nil {

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
@ -104,3 +105,24 @@ Error #03: third
assert.Nil(t, errs.JSON()) assert.Nil(t, errs.JSON())
assert.Empty(t, errs.String()) assert.Empty(t, errs.String())
} }
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("somme error")
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
err := fmt.Errorf("wrapped: %w", &Error{
Err: innerErr,
Type: ErrorTypeAny,
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
}

8
fs.go
View File

@ -9,7 +9,7 @@ import (
"os" "os"
) )
type onlyfilesFS struct { type onlyFilesFS struct {
fs http.FileSystem fs http.FileSystem
} }
@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
http.File http.File
} }
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally // Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
// in router.Static(). // in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files. // a filesystem that prevents http.FileServer() to list the directory files.
@ -26,11 +26,11 @@ func Dir(root string, listDirectory bool) http.FileSystem {
if listDirectory { if listDirectory {
return fs return fs
} }
return &onlyfilesFS{fs} return &onlyFilesFS{fs}
} }
// Open conforms to http.Filesystem. // Open conforms to http.Filesystem.
func (fs onlyfilesFS) Open(name string) (http.File, error) { func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name) f, err := fs.fs.Open(name)
if err != nil { if err != nil {
return nil, err return nil, err

227
gin.go
View File

@ -11,26 +11,29 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"strings"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const defaultMultipartMemory = 32 << 20 // 32 MB
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
default405Body = []byte("405 method not allowed") default405Body = []byte("405 method not allowed")
defaultAppEngine bool
) )
var defaultPlatform string
// HandlerFunc defines the handler used by gin middleware as return value. // HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array. // HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own. // Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc { func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 { if length := len(c); length > 0 {
return c[length-1] return c[length-1]
@ -49,6 +52,16 @@ type RouteInfo struct {
// RoutesInfo defines a RouteInfo array. // RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo type RoutesInfo []RouteInfo
// Trusted platforms
const (
// When running on Google App Engine. Trust X-Appengine-Remote-Addr
// for determining the client's IP
PlatformGoogleAppEngine = "google-app-engine"
// When using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP
PlatformCloudflare = "cloudflare"
)
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
type Engine struct { type Engine struct {
@ -79,9 +92,15 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound // If no other Method is allowed, the request is delegated to the NotFound
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with // If enabled, client IP will be parsed from the request's headers that
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
// fetched, it falls back to the IP obtained from
// `(*gin.Context).Request.RemoteAddr`.
ForwardedByClientIP bool
// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
// #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS. // 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool AppEngine bool
@ -93,12 +112,32 @@ type Engine struct {
// as url.Path gonna be used, which is already unescaped. // as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool UnescapePathValues bool
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
// List of headers used to obtain the client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of `(*gin.Engine).TrustedProxies`.
RemoteIPHeaders []string
// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
// IPv6 CIDRs) from which to trust request's headers that contain
// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
// `true`.
TrustedProxies []string
// If set to a constant of value gin.Platform*, trusts the headers set by
// that platform, for example to determine the client IP
TrustedPlatform string
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call. // method call.
MaxMultipartMemory int64 MaxMultipartMemory int64
delims render.Delims delims render.Delims
secureJsonPrefix string secureJSONPrefix string
HTMLRender render.HTMLRender HTMLRender render.HTMLRender
FuncMap template.FuncMap FuncMap template.FuncMap
allNoRoute HandlersChain allNoRoute HandlersChain
@ -107,6 +146,8 @@ type Engine struct {
noMethod HandlersChain noMethod HandlersChain
pool sync.Pool pool sync.Pool
trees methodTrees trees methodTrees
maxParams uint16
trustedCIDRs []*net.IPNet
} }
var _ IRouter = &Engine{} var _ IRouter = &Engine{}
@ -132,13 +173,16 @@ func New() *Engine {
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
AppEngine: defaultAppEngine, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
TrustedPlatform: defaultPlatform,
UseRawPath: false, UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory, MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"}, delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);", secureJSONPrefix: "while(1);",
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { engine.pool.New = func() interface{} {
@ -156,7 +200,8 @@ func Default() *Engine {
} }
func (engine *Engine) allocateContext() *Context { func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine} v := make(Params, 0, engine.maxParams)
return &Context{engine: engine, params: &v}
} }
// Delims sets template left and right delims and returns a Engine instance. // Delims sets template left and right delims and returns a Engine instance.
@ -165,9 +210,9 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine return engine
} }
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON. // SecureJsonPrefix sets the secureJSONPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix engine.secureJSONPrefix = prefix
return engine return engine
} }
@ -249,12 +294,19 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(len(handlers) > 0, "there must be at least one handler") assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers) debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) root := engine.trees.get(method)
if root == nil { if root == nil {
root = new(node) root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root}) engine.trees = append(engine.trees, methodTree{method: method, root: root})
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
} }
// Routes returns a slice of registered routes, including some useful information, such as: // Routes returns a slice of registered routes, including some useful information, such as:
@ -289,12 +341,18 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
address := resolveAddress(addr) address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine) err = http.ListenAndServe(address, engine)
return return
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests. // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// You can use your http.server config like ReadTimeout ... // You can use your http.server config like ReadTimeout ...
// It is a shortcut for server.ListenAndServe() // It is a shortcut for server.ListenAndServe()
@ -311,6 +369,61 @@ func (engine *Engine) RunServer(server *http.Server, addr ...string) (err error)
err = server.ListenAndServe() err = server.ListenAndServe()
return return
} }
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.TrustedProxies == nil {
return nil, nil
}
cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
for _, trustedProxy := range engine.TrustedProxies {
if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy)
if ip == nil {
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
}
switch len(ip) {
case net.IPv4len:
trustedProxy += "/32"
case net.IPv6len:
trustedProxy += "/128"
}
}
_, cidrNet, err := net.ParseCIDR(trustedProxy)
if err != nil {
return cidr, err
}
cidr = append(cidr, cidrNet)
}
return cidr, nil
}
// SetTrustedProxies set Engine.TrustedProxies
func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
engine.TrustedProxies = trustedProxies
return engine.parseTrustedProxies()
}
// parseTrustedProxies parse Engine.TrustedProxies to Engine.trustedCIDRs
func (engine *Engine) parseTrustedProxies() error {
trustedCIDRs, err := engine.prepareTrustedCIDRs()
engine.trustedCIDRs = trustedCIDRs
return err
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
parsedIP := net.ParseIP(ip)
if ipv4 := parsedIP.To4(); ipv4 != nil {
// return ip in a 4-byte representation
return ipv4
}
// return ip in a 16-byte representation or nil
return parsedIP
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
@ -319,6 +432,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr) debugPrint("Listening and serving HTTPS on %s\n", addr)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine) err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
return return
} }
@ -330,13 +448,18 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file) debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
os.Remove(file) err = engine.parseTrustedProxies()
if err != nil {
return err
}
listener, err := net.Listen("unix", file) listener, err := net.Listen("unix", file)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
os.Chmod(file, 0777) defer os.Remove(file)
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -348,12 +471,32 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd) debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f) listener, err := net.FileListener(f)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
err = engine.RunListener(listener)
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -389,7 +532,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = c.Request.URL.RawPath rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
rPath = cleanPath(rPath)
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
@ -399,16 +545,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
handlers, params, tsr := root.getValue(rPath, c.Params, unescape) value := root.getValue(rPath, c.params, unescape)
if handlers != nil { if value.params != nil {
c.handlers = handlers c.Params = *value.params
c.Params = params }
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() c.Next()
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != "CONNECT" && rPath != "/" { if httpMethod != http.MethodConnect && rPath != "/" {
if tsr && engine.RedirectTrailingSlash { if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c) redirectTrailingSlash(c)
return return
} }
@ -424,7 +573,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod { if tree.method == httpMethod {
continue continue
} }
if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil { if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body) serveError(c, http.StatusMethodNotAllowed, default405Body)
return return
@ -452,7 +601,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
return return
} }
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return
} }
func redirectTrailingSlash(c *Context) { func redirectTrailingSlash(c *Context) {
@ -461,18 +609,11 @@ func redirectTrailingSlash(c *Context) {
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
p = prefix + "/" + req.URL.Path p = prefix + "/" + req.URL.Path
} }
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
}
req.URL.Path = p + "/" req.URL.Path = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' { if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1] req.URL.Path = p[:length-1]
} }
debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String()) redirectRequest(c)
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
} }
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
@ -480,15 +621,23 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
rPath := req.URL.Path rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok { if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method req.URL.Path = bytesconv.BytesToString(fixedPath)
if req.Method != "GET" { redirectRequest(c)
code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
return true return true
} }
return false return false
} }
func redirectRequest(c *Context) {
req := c.Request
rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != http.MethodGet {
code = http.StatusTemporaryRedirect
}
debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()
}

View File

@ -14,6 +14,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -21,7 +22,15 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testRequest(t *testing.T, url string) { // params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) {
if len(params) == 0 {
t.Fatal("url cannot be empty")
}
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -29,14 +38,27 @@ func testRequest(t *testing.T, url string) {
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get(url) resp, err := client.Get(params[0])
assert.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body) body, ioerr := ioutil.ReadAll(resp.Body)
assert.NoError(t, ioerr) assert.NoError(t, ioerr)
assert.Equal(t, "it worked", string(body), "resp body should match")
assert.Equal(t, "200 OK", resp.Status, "should get a 200") var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
var responseBody = "it worked"
if len(params) > 2 && params[2] != "" {
responseBody = params[2]
}
assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
if responseStatus == "200 OK" {
assert.Equal(t, responseBody, string(body), "resp body should match")
}
} }
func TestRunEmpty(t *testing.T) { func TestRunEmpty(t *testing.T) {
@ -54,8 +76,76 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestRunServer(t *testing.T) {
func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
}
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}
func TestRunTLS(t *testing.T) {
router := New() router := New()
service := &http.Server{} service := &http.Server{}
go func() { go func() {
@ -91,7 +181,8 @@ func TestPusher(t *testing.T) {
go func() { go func() {
router.GET("/pusher", func(c *Context) { router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil { if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/app.js", nil) err := pusher.Push("/assets/app.js", nil)
assert.NoError(t, err)
} }
c.String(http.StatusOK, "it worked") c.String(http.StatusOK, "it worked")
}) })
@ -146,15 +237,19 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) { func TestUnixSocket(t *testing.T) {
router := New() router := New()
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunUnix("/tmp/unix_unit_test")) assert.NoError(t, router.RunUnix(unixTestSocket))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", "/tmp/unix_unit_test") c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err) assert.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
@ -208,6 +303,43 @@ func TestBadFileDescriptor(t *testing.T) {
assert.Error(t, router.RunFd(0)) assert.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
listener.Close()
assert.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New() router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -254,7 +386,7 @@ func TestConcurrentHandleContext(t *testing.T) {
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) { func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err) assert.NoError(t, err)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -263,3 +395,134 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
assert.Equal(t, "it worked", w.Body.String(), "resp body should match") assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200") assert.Equal(t, 200, w.Code, "should get a 200")
} }
func TestTreeRunDynamicRouting(t *testing.T) {
router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
router.GET("/get/abc", func(c *Context) { c.String(http.StatusOK, "/get/abc") })
router.GET("/get/:param", func(c *Context) { c.String(http.StatusOK, "/get/:param") })
router.GET("/get/abc/123abc", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc") })
router.GET("/get/abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param") })
router.GET("/get/abc/123abc/xxx8", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8") })
router.GET("/get/abc/123abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/:param") })
router.GET("/get/abc/123abc/xxx8/1234", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234") })
router.GET("/get/abc/123abc/xxx8/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/:param") })
router.GET("/get/abc/123abc/xxx8/1234/ffas", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/ffas") })
router.GET("/get/abc/123abc/xxx8/1234/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/:param") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/12c", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/12c") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/:param") })
router.GET("/get/abc/:param/test", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param/test") })
router.GET("/get/abc/123abd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abd/:param") })
router.GET("/get/abc/123abddd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abddd/:param") })
router.GET("/get/abc/123/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123/:param") })
router.GET("/get/abc/123abg/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abg/:param") })
router.GET("/get/abc/123abf/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abf/:param") })
router.GET("/get/abc/123abfff/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abfff/:param") })
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/", "", "home")
testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
testRequest(t, ts.URL+"/all", "", "/:cc")
testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/a", "", "/:cc")
testRequest(t, ts.URL+"/d", "", "/:cc")
testRequest(t, ts.URL+"/ad", "", "/:cc")
testRequest(t, ts.URL+"/dd", "", "/:cc")
testRequest(t, ts.URL+"/aa", "", "/:cc")
testRequest(t, ts.URL+"/aaa", "", "/:cc")
testRequest(t, ts.URL+"/aaa/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ab", "", "/:cc")
testRequest(t, ts.URL+"/abb", "", "/:cc")
testRequest(t, ts.URL+"/abb/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/dddaa", "", "/:cc")
testRequest(t, ts.URL+"/allxxxx", "", "/:cc")
testRequest(t, ts.URL+"/alldd", "", "/:cc")
testRequest(t, ts.URL+"/cc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ccc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/deedwjfs/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/acllcc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
testRequest(t, ts.URL+"/get/testaa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/get/abc", "", "/get/abc")
testRequest(t, ts.URL+"/get/a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abz", "", "/get/:param")
testRequest(t, ts.URL+"/get/12a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abcd", "", "/get/:param")
testRequest(t, ts.URL+"/get/abc/123abc", "", "/get/abc/123abc")
testRequest(t, ts.URL+"/get/abc/12", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123ab", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/xyz", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abcddxx", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8", "", "/get/abc/123abc/xxx8")
testRequest(t, ts.URL+"/get/abc/123abc/x", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/abc", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8xxas", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234", "", "/get/abc/123abc/xxx8/1234")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/123", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/78k", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234xxxd", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas", "", "/get/abc/123abc/xxx8/1234/ffas")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/f", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffa", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kka", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas321", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c", "", "/get/abc/123abc/xxx8/1234/kkdd/12c")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/1", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12b", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/34", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/12/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdd/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdddf/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123ab/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abgg/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abffff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abd/test", "", "/get/abc/123abd/:param")
testRequest(t, ts.URL+"/get/abc/123abddd/test", "", "/get/abc/123abddd/:param")
testRequest(t, ts.URL+"/get/abc/123/test22", "", "/get/abc/123/:param")
testRequest(t, ts.URL+"/get/abc/123abg/test", "", "/get/abc/123abg/:param")
testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param")
testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param")
// 404 not found
testRequest(t, ts.URL+"/a/dd", "404 Not Found")
testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
}

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@ -532,6 +533,139 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter) assert.Equal(t, int64(expectValue), middlewareCounter)
} }
func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New()
// valid ipv4 cidr
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
r.TrustedProxies = []string{"0.0.0.0/0"}
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
}
// invalid ipv4 cidr
{
r.TrustedProxies = []string{"192.168.1.33/33"}
_, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
}
// valid ipv4 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
r.TrustedProxies = []string{"192.168.1.33"}
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
}
// invalid ipv4 address
{
r.TrustedProxies = []string{"192.168.1.256"}
_, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
}
// valid ipv6 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
r.TrustedProxies = []string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"}
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
}
// invalid ipv6 address
{
r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}
_, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
}
// valid ipv6 cidr
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
r.TrustedProxies = []string{"::/0"}
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
}
// invalid ipv6 cidr
{
r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}
_, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
}
// valid combination
{
expectedTrustedCIDRs := []*net.IPNet{
parseCIDR("::/0"),
parseCIDR("192.168.0.0/16"),
parseCIDR("172.16.0.1/32"),
}
r.TrustedProxies = []string{
"::/0",
"192.168.0.0/16",
"172.16.0.1",
}
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
}
// invalid combination
{
r.TrustedProxies = []string{
"::/0",
"192.168.0.0/16",
"172.16.0.256",
}
_, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
}
// nil value
{
r.TrustedProxies = nil
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Nil(t, trustedCIDRs)
assert.Nil(t, err)
}
}
func parseCIDR(cidr string) *net.IPNet {
_, parsedCIDR, err := net.ParseCIDR(cidr)
if err != nil {
fmt.Println(err)
}
return parsedCIDR
}
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) { func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes { for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

View File

@ -24,265 +24,265 @@ type route struct {
// http://developer.github.com/v3/ // http://developer.github.com/v3/
var githubAPI = []route{ var githubAPI = []route{
// OAuth Authorizations // OAuth Authorizations
{"GET", "/authorizations"}, {http.MethodGet, "/authorizations"},
{"GET", "/authorizations/:id"}, {http.MethodGet, "/authorizations/:id"},
{"POST", "/authorizations"}, {http.MethodPost, "/authorizations"},
//{"PUT", "/authorizations/clients/:client_id"}, //{http.MethodPut, "/authorizations/clients/:client_id"},
//{"PATCH", "/authorizations/:id"}, //{http.MethodPatch, "/authorizations/:id"},
{"DELETE", "/authorizations/:id"}, {http.MethodDelete, "/authorizations/:id"},
{"GET", "/applications/:client_id/tokens/:access_token"}, {http.MethodGet, "/applications/:client_id/tokens/:access_token"},
{"DELETE", "/applications/:client_id/tokens"}, {http.MethodDelete, "/applications/:client_id/tokens"},
{"DELETE", "/applications/:client_id/tokens/:access_token"}, {http.MethodDelete, "/applications/:client_id/tokens/:access_token"},
// Activity // Activity
{"GET", "/events"}, {http.MethodGet, "/events"},
{"GET", "/repos/:owner/:repo/events"}, {http.MethodGet, "/repos/:owner/:repo/events"},
{"GET", "/networks/:owner/:repo/events"}, {http.MethodGet, "/networks/:owner/:repo/events"},
{"GET", "/orgs/:org/events"}, {http.MethodGet, "/orgs/:org/events"},
{"GET", "/users/:user/received_events"}, {http.MethodGet, "/users/:user/received_events"},
{"GET", "/users/:user/received_events/public"}, {http.MethodGet, "/users/:user/received_events/public"},
{"GET", "/users/:user/events"}, {http.MethodGet, "/users/:user/events"},
{"GET", "/users/:user/events/public"}, {http.MethodGet, "/users/:user/events/public"},
{"GET", "/users/:user/events/orgs/:org"}, {http.MethodGet, "/users/:user/events/orgs/:org"},
{"GET", "/feeds"}, {http.MethodGet, "/feeds"},
{"GET", "/notifications"}, {http.MethodGet, "/notifications"},
{"GET", "/repos/:owner/:repo/notifications"}, {http.MethodGet, "/repos/:owner/:repo/notifications"},
{"PUT", "/notifications"}, {http.MethodPut, "/notifications"},
{"PUT", "/repos/:owner/:repo/notifications"}, {http.MethodPut, "/repos/:owner/:repo/notifications"},
{"GET", "/notifications/threads/:id"}, {http.MethodGet, "/notifications/threads/:id"},
//{"PATCH", "/notifications/threads/:id"}, //{http.MethodPatch, "/notifications/threads/:id"},
{"GET", "/notifications/threads/:id/subscription"}, {http.MethodGet, "/notifications/threads/:id/subscription"},
{"PUT", "/notifications/threads/:id/subscription"}, {http.MethodPut, "/notifications/threads/:id/subscription"},
{"DELETE", "/notifications/threads/:id/subscription"}, {http.MethodDelete, "/notifications/threads/:id/subscription"},
{"GET", "/repos/:owner/:repo/stargazers"}, {http.MethodGet, "/repos/:owner/:repo/stargazers"},
{"GET", "/users/:user/starred"}, {http.MethodGet, "/users/:user/starred"},
{"GET", "/user/starred"}, {http.MethodGet, "/user/starred"},
{"GET", "/user/starred/:owner/:repo"}, {http.MethodGet, "/user/starred/:owner/:repo"},
{"PUT", "/user/starred/:owner/:repo"}, {http.MethodPut, "/user/starred/:owner/:repo"},
{"DELETE", "/user/starred/:owner/:repo"}, {http.MethodDelete, "/user/starred/:owner/:repo"},
{"GET", "/repos/:owner/:repo/subscribers"}, {http.MethodGet, "/repos/:owner/:repo/subscribers"},
{"GET", "/users/:user/subscriptions"}, {http.MethodGet, "/users/:user/subscriptions"},
{"GET", "/user/subscriptions"}, {http.MethodGet, "/user/subscriptions"},
{"GET", "/repos/:owner/:repo/subscription"}, {http.MethodGet, "/repos/:owner/:repo/subscription"},
{"PUT", "/repos/:owner/:repo/subscription"}, {http.MethodPut, "/repos/:owner/:repo/subscription"},
{"DELETE", "/repos/:owner/:repo/subscription"}, {http.MethodDelete, "/repos/:owner/:repo/subscription"},
{"GET", "/user/subscriptions/:owner/:repo"}, {http.MethodGet, "/user/subscriptions/:owner/:repo"},
{"PUT", "/user/subscriptions/:owner/:repo"}, {http.MethodPut, "/user/subscriptions/:owner/:repo"},
{"DELETE", "/user/subscriptions/:owner/:repo"}, {http.MethodDelete, "/user/subscriptions/:owner/:repo"},
// Gists // Gists
{"GET", "/users/:user/gists"}, {http.MethodGet, "/users/:user/gists"},
{"GET", "/gists"}, {http.MethodGet, "/gists"},
//{"GET", "/gists/public"}, //{http.MethodGet, "/gists/public"},
//{"GET", "/gists/starred"}, //{http.MethodGet, "/gists/starred"},
{"GET", "/gists/:id"}, {http.MethodGet, "/gists/:id"},
{"POST", "/gists"}, {http.MethodPost, "/gists"},
//{"PATCH", "/gists/:id"}, //{http.MethodPatch, "/gists/:id"},
{"PUT", "/gists/:id/star"}, {http.MethodPut, "/gists/:id/star"},
{"DELETE", "/gists/:id/star"}, {http.MethodDelete, "/gists/:id/star"},
{"GET", "/gists/:id/star"}, {http.MethodGet, "/gists/:id/star"},
{"POST", "/gists/:id/forks"}, {http.MethodPost, "/gists/:id/forks"},
{"DELETE", "/gists/:id"}, {http.MethodDelete, "/gists/:id"},
// Git Data // Git Data
{"GET", "/repos/:owner/:repo/git/blobs/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/blobs/:sha"},
{"POST", "/repos/:owner/:repo/git/blobs"}, {http.MethodPost, "/repos/:owner/:repo/git/blobs"},
{"GET", "/repos/:owner/:repo/git/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/commits/:sha"},
{"POST", "/repos/:owner/:repo/git/commits"}, {http.MethodPost, "/repos/:owner/:repo/git/commits"},
//{"GET", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodGet, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/refs"}, {http.MethodGet, "/repos/:owner/:repo/git/refs"},
{"POST", "/repos/:owner/:repo/git/refs"}, {http.MethodPost, "/repos/:owner/:repo/git/refs"},
//{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodPatch, "/repos/:owner/:repo/git/refs/*ref"},
//{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodDelete, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/tags/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/tags/:sha"},
{"POST", "/repos/:owner/:repo/git/tags"}, {http.MethodPost, "/repos/:owner/:repo/git/tags"},
{"GET", "/repos/:owner/:repo/git/trees/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/trees/:sha"},
{"POST", "/repos/:owner/:repo/git/trees"}, {http.MethodPost, "/repos/:owner/:repo/git/trees"},
// Issues // Issues
{"GET", "/issues"}, {http.MethodGet, "/issues"},
{"GET", "/user/issues"}, {http.MethodGet, "/user/issues"},
{"GET", "/orgs/:org/issues"}, {http.MethodGet, "/orgs/:org/issues"},
{"GET", "/repos/:owner/:repo/issues"}, {http.MethodGet, "/repos/:owner/:repo/issues"},
{"GET", "/repos/:owner/:repo/issues/:number"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number"},
{"POST", "/repos/:owner/:repo/issues"}, {http.MethodPost, "/repos/:owner/:repo/issues"},
//{"PATCH", "/repos/:owner/:repo/issues/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/:number"},
{"GET", "/repos/:owner/:repo/assignees"}, {http.MethodGet, "/repos/:owner/:repo/assignees"},
{"GET", "/repos/:owner/:repo/assignees/:assignee"}, {http.MethodGet, "/repos/:owner/:repo/assignees/:assignee"},
{"GET", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments/:id"},
{"POST", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/comments/:id"},
//{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodDelete, "/repos/:owner/:repo/issues/comments/:id"},
{"GET", "/repos/:owner/:repo/issues/:number/events"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/events"},
//{"GET", "/repos/:owner/:repo/issues/events"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events"},
//{"GET", "/repos/:owner/:repo/issues/events/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events/:id"},
{"GET", "/repos/:owner/:repo/labels"}, {http.MethodGet, "/repos/:owner/:repo/labels"},
{"GET", "/repos/:owner/:repo/labels/:name"}, {http.MethodGet, "/repos/:owner/:repo/labels/:name"},
{"POST", "/repos/:owner/:repo/labels"}, {http.MethodPost, "/repos/:owner/:repo/labels"},
//{"PATCH", "/repos/:owner/:repo/labels/:name"}, //{http.MethodPatch, "/repos/:owner/:repo/labels/:name"},
{"DELETE", "/repos/:owner/:repo/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/labels/:name"},
{"GET", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/labels"},
{"POST", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels/:name"},
{"PUT", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPut, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones"}, {http.MethodGet, "/repos/:owner/:repo/milestones"},
{"GET", "/repos/:owner/:repo/milestones/:number"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number"},
{"POST", "/repos/:owner/:repo/milestones"}, {http.MethodPost, "/repos/:owner/:repo/milestones"},
//{"PATCH", "/repos/:owner/:repo/milestones/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/milestones/:number"},
{"DELETE", "/repos/:owner/:repo/milestones/:number"}, {http.MethodDelete, "/repos/:owner/:repo/milestones/:number"},
// Miscellaneous // Miscellaneous
{"GET", "/emojis"}, {http.MethodGet, "/emojis"},
{"GET", "/gitignore/templates"}, {http.MethodGet, "/gitignore/templates"},
{"GET", "/gitignore/templates/:name"}, {http.MethodGet, "/gitignore/templates/:name"},
{"POST", "/markdown"}, {http.MethodPost, "/markdown"},
{"POST", "/markdown/raw"}, {http.MethodPost, "/markdown/raw"},
{"GET", "/meta"}, {http.MethodGet, "/meta"},
{"GET", "/rate_limit"}, {http.MethodGet, "/rate_limit"},
// Organizations // Organizations
{"GET", "/users/:user/orgs"}, {http.MethodGet, "/users/:user/orgs"},
{"GET", "/user/orgs"}, {http.MethodGet, "/user/orgs"},
{"GET", "/orgs/:org"}, {http.MethodGet, "/orgs/:org"},
//{"PATCH", "/orgs/:org"}, //{http.MethodPatch, "/orgs/:org"},
{"GET", "/orgs/:org/members"}, {http.MethodGet, "/orgs/:org/members"},
{"GET", "/orgs/:org/members/:user"}, {http.MethodGet, "/orgs/:org/members/:user"},
{"DELETE", "/orgs/:org/members/:user"}, {http.MethodDelete, "/orgs/:org/members/:user"},
{"GET", "/orgs/:org/public_members"}, {http.MethodGet, "/orgs/:org/public_members"},
{"GET", "/orgs/:org/public_members/:user"}, {http.MethodGet, "/orgs/:org/public_members/:user"},
{"PUT", "/orgs/:org/public_members/:user"}, {http.MethodPut, "/orgs/:org/public_members/:user"},
{"DELETE", "/orgs/:org/public_members/:user"}, {http.MethodDelete, "/orgs/:org/public_members/:user"},
{"GET", "/orgs/:org/teams"}, {http.MethodGet, "/orgs/:org/teams"},
{"GET", "/teams/:id"}, {http.MethodGet, "/teams/:id"},
{"POST", "/orgs/:org/teams"}, {http.MethodPost, "/orgs/:org/teams"},
//{"PATCH", "/teams/:id"}, //{http.MethodPatch, "/teams/:id"},
{"DELETE", "/teams/:id"}, {http.MethodDelete, "/teams/:id"},
{"GET", "/teams/:id/members"}, {http.MethodGet, "/teams/:id/members"},
{"GET", "/teams/:id/members/:user"}, {http.MethodGet, "/teams/:id/members/:user"},
{"PUT", "/teams/:id/members/:user"}, {http.MethodPut, "/teams/:id/members/:user"},
{"DELETE", "/teams/:id/members/:user"}, {http.MethodDelete, "/teams/:id/members/:user"},
{"GET", "/teams/:id/repos"}, {http.MethodGet, "/teams/:id/repos"},
{"GET", "/teams/:id/repos/:owner/:repo"}, {http.MethodGet, "/teams/:id/repos/:owner/:repo"},
{"PUT", "/teams/:id/repos/:owner/:repo"}, {http.MethodPut, "/teams/:id/repos/:owner/:repo"},
{"DELETE", "/teams/:id/repos/:owner/:repo"}, {http.MethodDelete, "/teams/:id/repos/:owner/:repo"},
{"GET", "/user/teams"}, {http.MethodGet, "/user/teams"},
// Pull Requests // Pull Requests
{"GET", "/repos/:owner/:repo/pulls"}, {http.MethodGet, "/repos/:owner/:repo/pulls"},
{"GET", "/repos/:owner/:repo/pulls/:number"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number"},
{"POST", "/repos/:owner/:repo/pulls"}, {http.MethodPost, "/repos/:owner/:repo/pulls"},
//{"PATCH", "/repos/:owner/:repo/pulls/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/:number"},
{"GET", "/repos/:owner/:repo/pulls/:number/commits"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/commits"},
{"GET", "/repos/:owner/:repo/pulls/:number/files"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/files"},
{"GET", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/merge"},
{"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/merge"},
{"GET", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments/:number"},
{"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/comments/:number"},
//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodDelete, "/repos/:owner/:repo/pulls/comments/:number"},
// Repositories // Repositories
{"GET", "/user/repos"}, {http.MethodGet, "/user/repos"},
{"GET", "/users/:user/repos"}, {http.MethodGet, "/users/:user/repos"},
{"GET", "/orgs/:org/repos"}, {http.MethodGet, "/orgs/:org/repos"},
{"GET", "/repositories"}, {http.MethodGet, "/repositories"},
{"POST", "/user/repos"}, {http.MethodPost, "/user/repos"},
{"POST", "/orgs/:org/repos"}, {http.MethodPost, "/orgs/:org/repos"},
{"GET", "/repos/:owner/:repo"}, {http.MethodGet, "/repos/:owner/:repo"},
//{"PATCH", "/repos/:owner/:repo"}, //{http.MethodPatch, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/contributors"}, {http.MethodGet, "/repos/:owner/:repo/contributors"},
{"GET", "/repos/:owner/:repo/languages"}, {http.MethodGet, "/repos/:owner/:repo/languages"},
{"GET", "/repos/:owner/:repo/teams"}, {http.MethodGet, "/repos/:owner/:repo/teams"},
{"GET", "/repos/:owner/:repo/tags"}, {http.MethodGet, "/repos/:owner/:repo/tags"},
{"GET", "/repos/:owner/:repo/branches"}, {http.MethodGet, "/repos/:owner/:repo/branches"},
{"GET", "/repos/:owner/:repo/branches/:branch"}, {http.MethodGet, "/repos/:owner/:repo/branches/:branch"},
{"DELETE", "/repos/:owner/:repo"}, {http.MethodDelete, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/collaborators"}, {http.MethodGet, "/repos/:owner/:repo/collaborators"},
{"GET", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodGet, "/repos/:owner/:repo/collaborators/:user"},
{"PUT", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodPut, "/repos/:owner/:repo/collaborators/:user"},
{"DELETE", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodDelete, "/repos/:owner/:repo/collaborators/:user"},
{"GET", "/repos/:owner/:repo/comments"}, {http.MethodGet, "/repos/:owner/:repo/comments"},
{"GET", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha/comments"},
{"POST", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodPost, "/repos/:owner/:repo/commits/:sha/comments"},
{"GET", "/repos/:owner/:repo/comments/:id"}, {http.MethodGet, "/repos/:owner/:repo/comments/:id"},
//{"PATCH", "/repos/:owner/:repo/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/comments/:id"},
{"DELETE", "/repos/:owner/:repo/comments/:id"}, {http.MethodDelete, "/repos/:owner/:repo/comments/:id"},
{"GET", "/repos/:owner/:repo/commits"}, {http.MethodGet, "/repos/:owner/:repo/commits"},
{"GET", "/repos/:owner/:repo/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha"},
{"GET", "/repos/:owner/:repo/readme"}, {http.MethodGet, "/repos/:owner/:repo/readme"},
//{"GET", "/repos/:owner/:repo/contents/*path"}, //{http.MethodGet, "/repos/:owner/:repo/contents/*path"},
//{"PUT", "/repos/:owner/:repo/contents/*path"}, //{http.MethodPut, "/repos/:owner/:repo/contents/*path"},
//{"DELETE", "/repos/:owner/:repo/contents/*path"}, //{http.MethodDelete, "/repos/:owner/:repo/contents/*path"},
//{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, //{http.MethodGet, "/repos/:owner/:repo/:archive_format/:ref"},
{"GET", "/repos/:owner/:repo/keys"}, {http.MethodGet, "/repos/:owner/:repo/keys"},
{"GET", "/repos/:owner/:repo/keys/:id"}, {http.MethodGet, "/repos/:owner/:repo/keys/:id"},
{"POST", "/repos/:owner/:repo/keys"}, {http.MethodPost, "/repos/:owner/:repo/keys"},
//{"PATCH", "/repos/:owner/:repo/keys/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/keys/:id"},
{"DELETE", "/repos/:owner/:repo/keys/:id"}, {http.MethodDelete, "/repos/:owner/:repo/keys/:id"},
{"GET", "/repos/:owner/:repo/downloads"}, {http.MethodGet, "/repos/:owner/:repo/downloads"},
{"GET", "/repos/:owner/:repo/downloads/:id"}, {http.MethodGet, "/repos/:owner/:repo/downloads/:id"},
{"DELETE", "/repos/:owner/:repo/downloads/:id"}, {http.MethodDelete, "/repos/:owner/:repo/downloads/:id"},
{"GET", "/repos/:owner/:repo/forks"}, {http.MethodGet, "/repos/:owner/:repo/forks"},
{"POST", "/repos/:owner/:repo/forks"}, {http.MethodPost, "/repos/:owner/:repo/forks"},
{"GET", "/repos/:owner/:repo/hooks"}, {http.MethodGet, "/repos/:owner/:repo/hooks"},
{"GET", "/repos/:owner/:repo/hooks/:id"}, {http.MethodGet, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks"}, {http.MethodPost, "/repos/:owner/:repo/hooks"},
//{"PATCH", "/repos/:owner/:repo/hooks/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks/:id/tests"}, {http.MethodPost, "/repos/:owner/:repo/hooks/:id/tests"},
{"DELETE", "/repos/:owner/:repo/hooks/:id"}, {http.MethodDelete, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/merges"}, {http.MethodPost, "/repos/:owner/:repo/merges"},
{"GET", "/repos/:owner/:repo/releases"}, {http.MethodGet, "/repos/:owner/:repo/releases"},
{"GET", "/repos/:owner/:repo/releases/:id"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id"},
{"POST", "/repos/:owner/:repo/releases"}, {http.MethodPost, "/repos/:owner/:repo/releases"},
//{"PATCH", "/repos/:owner/:repo/releases/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/releases/:id"},
{"DELETE", "/repos/:owner/:repo/releases/:id"}, {http.MethodDelete, "/repos/:owner/:repo/releases/:id"},
{"GET", "/repos/:owner/:repo/releases/:id/assets"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id/assets"},
{"GET", "/repos/:owner/:repo/stats/contributors"}, {http.MethodGet, "/repos/:owner/:repo/stats/contributors"},
{"GET", "/repos/:owner/:repo/stats/commit_activity"}, {http.MethodGet, "/repos/:owner/:repo/stats/commit_activity"},
{"GET", "/repos/:owner/:repo/stats/code_frequency"}, {http.MethodGet, "/repos/:owner/:repo/stats/code_frequency"},
{"GET", "/repos/:owner/:repo/stats/participation"}, {http.MethodGet, "/repos/:owner/:repo/stats/participation"},
{"GET", "/repos/:owner/:repo/stats/punch_card"}, {http.MethodGet, "/repos/:owner/:repo/stats/punch_card"},
{"GET", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodGet, "/repos/:owner/:repo/statuses/:ref"},
{"POST", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodPost, "/repos/:owner/:repo/statuses/:ref"},
// Search // Search
{"GET", "/search/repositories"}, {http.MethodGet, "/search/repositories"},
{"GET", "/search/code"}, {http.MethodGet, "/search/code"},
{"GET", "/search/issues"}, {http.MethodGet, "/search/issues"},
{"GET", "/search/users"}, {http.MethodGet, "/search/users"},
{"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, {http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword"},
{"GET", "/legacy/repos/search/:keyword"}, {http.MethodGet, "/legacy/repos/search/:keyword"},
{"GET", "/legacy/user/search/:keyword"}, {http.MethodGet, "/legacy/user/search/:keyword"},
{"GET", "/legacy/user/email/:email"}, {http.MethodGet, "/legacy/user/email/:email"},
// Users // Users
{"GET", "/users/:user"}, {http.MethodGet, "/users/:user"},
{"GET", "/user"}, {http.MethodGet, "/user"},
//{"PATCH", "/user"}, //{http.MethodPatch, "/user"},
{"GET", "/users"}, {http.MethodGet, "/users"},
{"GET", "/user/emails"}, {http.MethodGet, "/user/emails"},
{"POST", "/user/emails"}, {http.MethodPost, "/user/emails"},
{"DELETE", "/user/emails"}, {http.MethodDelete, "/user/emails"},
{"GET", "/users/:user/followers"}, {http.MethodGet, "/users/:user/followers"},
{"GET", "/user/followers"}, {http.MethodGet, "/user/followers"},
{"GET", "/users/:user/following"}, {http.MethodGet, "/users/:user/following"},
{"GET", "/user/following"}, {http.MethodGet, "/user/following"},
{"GET", "/user/following/:user"}, {http.MethodGet, "/user/following/:user"},
{"GET", "/users/:user/following/:target_user"}, {http.MethodGet, "/users/:user/following/:target_user"},
{"PUT", "/user/following/:user"}, {http.MethodPut, "/user/following/:user"},
{"DELETE", "/user/following/:user"}, {http.MethodDelete, "/user/following/:user"},
{"GET", "/users/:user/keys"}, {http.MethodGet, "/users/:user/keys"},
{"GET", "/user/keys"}, {http.MethodGet, "/user/keys"},
{"GET", "/user/keys/:id"}, {http.MethodGet, "/user/keys/:id"},
{"POST", "/user/keys"}, {http.MethodPost, "/user/keys"},
//{"PATCH", "/user/keys/:id"}, //{http.MethodPatch, "/user/keys/:id"},
{"DELETE", "/user/keys/:id"}, {http.MethodDelete, "/user/keys/:id"},
} }
func TestShouldBindUri(t *testing.T) { func TestShouldBindUri(t *testing.T) {
@ -291,18 +291,18 @@ func TestShouldBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.ShouldBindUri(&person)) assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id) assert.True(t, "" != person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String()) assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -313,18 +313,18 @@ func TestBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.BindUri(&person)) assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id) assert.True(t, "" != person.ID)
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "BindUri test OK", w.Body.String()) assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -336,13 +336,13 @@ func TestBindUriError(t *testing.T) {
type Member struct { type Member struct {
Number string `uri:"num" binding:"required,uuid"` Number string `uri:"num" binding:"required,uuid"`
} }
router.Handle("GET", "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
assert.Error(t, c.BindUri(&m)) assert.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, "GET", path1) w1 := performRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code) assert.Equal(t, http.StatusBadRequest, w1.Code)
} }
@ -358,7 +358,7 @@ func TestRaceContextCopy(t *testing.T) {
go readWriteKeys(c.Copy()) go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics") c.String(http.StatusOK, "run OK, no panics")
}) })
w := performRequest(router, "GET", "/test/copy/race") w := performRequest(router, http.MethodGet, "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String()) assert.Equal(t, "run OK, no panics", w.Body.String())
} }
@ -438,7 +438,7 @@ func exampleFromPath(path string) (string, Params) {
func BenchmarkGithub(b *testing.B) { func BenchmarkGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword") runRequest(b, router, http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword")
} }
func BenchmarkParallelGithub(b *testing.B) { func BenchmarkParallelGithub(b *testing.B) {
@ -446,7 +446,7 @@ func BenchmarkParallelGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.
@ -462,7 +462,7 @@ func BenchmarkParallelGithubDefault(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.

23
go.mod
View File

@ -1,18 +1,15 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.12 go 1.13
require ( require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 github.com/gin-contrib/sse v0.1.0
github.com/golang/protobuf v1.3.1 github.com/go-playground/validator/v10 v10.6.1
github.com/json-iterator/go v1.1.6 github.com/goccy/go-json v0.5.1
github.com/mattn/go-isatty v0.0.7 github.com/golang/protobuf v1.3.3
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/json-iterator/go v1.1.9
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.4.0
github.com/ugorji/go v1.1.4 github.com/ugorji/go/codec v1.2.6
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c gopkg.in/yaml.v2 v2.2.8
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2
) )

69
go.sum
View File

@ -1,36 +1,55 @@
github.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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/goccy/go-json v0.5.1 h1:R9UYTOUvo7eIY9aeDMZ4L6OVtHaSr1k2No9W6MKjXrA=
github.com/goccy/go-json v0.5.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -0,0 +1,24 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package bytesconv
import (
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -0,0 +1,99 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package bytesconv
import (
"bytes"
"math/rand"
"strings"
"testing"
"time"
)
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
var testBytes = []byte(testString)
func rawBytesToStr(b []byte) string {
return string(b)
}
func rawStrToBytes(s string) []byte {
return []byte(s)
}
// go test -v
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
rand.Read(data)
if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match")
}
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{}
sb.Grow(n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
sb.WriteByte(letterBytes[idx])
i--
}
cache >>= letterIdxBits
remain--
}
return sb.String()
}
func TestStringToBytes(t *testing.T) {
for i := 0; i < 100; i++ {
s := RandStringBytesMaskImprSrcSB(64)
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
t.Fatal("don't match")
}
}
}
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawBytesToStr(testBytes)
}
}
func BenchmarkBytesConvBytesToStr(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesToString(testBytes)
}
}
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawStrToBytes(testString)
}
}
func BenchmarkBytesConvStrToBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
StringToBytes(testString)
}
}

23
internal/json/go_json.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go_json
// +build go_json
package json
import json "github.com/goccy/go-json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -2,7 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !jsoniter //go:build !jsoniter && !go_json
// +build !jsoniter,!go_json
package json package json

View File

@ -2,11 +2,12 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build jsoniter
// +build jsoniter // +build jsoniter
package json package json
import "github.com/json-iterator/go" import jsoniter "github.com/json-iterator/go"
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary

View File

@ -22,18 +22,19 @@ const (
forceColor forceColor
) )
var ( const (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) green = "\033[97;42m"
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) white = "\033[90;47m"
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) yellow = "\033[90;43m"
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) red = "\033[97;41m"
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) blue = "\033[97;44m"
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) magenta = "\033[97;45m"
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) cyan = "\033[97;46m"
reset = string([]byte{27, 91, 48, 109}) reset = "\033[0m"
consoleColorMode = autoColor
) )
var consoleColorMode = autoColor
// LoggerConfig defines the config for Logger middleware. // LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct { type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter // Optional. Default value is gin.defaultLogFormatter
@ -98,19 +99,19 @@ func (p *LogFormatterParams) MethodColor() string {
method := p.Method method := p.Method
switch method { switch method {
case "GET": case http.MethodGet:
return blue return blue
case "POST": case http.MethodPost:
return cyan return cyan
case "PUT": case http.MethodPut:
return yellow return yellow
case "DELETE": case http.MethodDelete:
return red return red
case "PATCH": case http.MethodPatch:
return green return green
case "HEAD": case http.MethodHead:
return magenta return magenta
case "OPTIONS": case http.MethodOptions:
return white return white
default: default:
return reset return reset
@ -137,10 +138,9 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
} }
if param.Latency > time.Minute { if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way param.Latency = param.Latency.Truncate(time.Second)
param.Latency = param.Latency - param.Latency%time.Second
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
param.Latency, param.Latency,

View File

@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
router := New() router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string { router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s", return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode, param.StatusCode,
param.Latency, param.Latency,
@ -185,6 +185,8 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
router := New() router := New()
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
router.Use(LoggerWithConfig(LoggerConfig{ router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer, Output: buffer,
Formatter: func(param LogFormatterParams) string { Formatter: func(param LogFormatterParams) string {
@ -275,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
isTerm: false, isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
} }
@ -291,14 +293,14 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor() return p.MethodColor()
} }
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red") assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green") assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white") assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color") assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
} }
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
@ -309,10 +311,10 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor() return p.StatusCodeColor()
} }
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") assert.Equal(t, red, colorForStatus(2), "other things should be red")
} }
func TestResetColor(t *testing.T) { func TestResetColor(t *testing.T) {

21
mode.go
View File

@ -22,6 +22,7 @@ const (
// TestMode indicates gin mode is test. // TestMode indicates gin mode is test.
TestMode = "test" TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
releaseCode releaseCode
@ -50,19 +51,21 @@ func init() {
// SetMode sets gin mode according to input string. // SetMode sets gin mode according to input string.
func SetMode(value string) { func SetMode(value string) {
if value == "" {
value = DebugMode
}
switch value { switch value {
case DebugMode, "": case DebugMode:
ginMode = debugCode ginMode = debugCode
case ReleaseMode: case ReleaseMode:
ginMode = releaseCode ginMode = releaseCode
case TestMode: case TestMode:
ginMode = testCode ginMode = testCode
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
if value == "" {
value = DebugMode
} }
modeName = value modeName = value
} }
@ -71,12 +74,18 @@ func DisableBindValidation() {
binding.Validator = nil binding.Validator = nil
} }
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to // EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumber to
// call the UseNumber method on the JSON Decoder instance. // call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() { func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true binding.EnableDecoderUseNumber = true
} }
// EnableJsonDecoderDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
// call the DisallowUnknownFields method on the JSON Decoder instance.
func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true
}
// Mode returns currently gin mode. // Mode returns currently gin mode.
func Mode() string { func Mode() string {
return modeName return modeName

View File

@ -40,8 +40,22 @@ func TestSetMode(t *testing.T) {
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })
} }
func TestDisableBindValidation(t *testing.T) {
v := binding.Validator
assert.NotNil(t, binding.Validator)
DisableBindValidation()
assert.Nil(t, binding.Validator)
binding.Validator = v
}
func TestEnableJsonDecoderUseNumber(t *testing.T) { func TestEnableJsonDecoderUseNumber(t *testing.T) {
assert.False(t, binding.EnableDecoderUseNumber) assert.False(t, binding.EnableDecoderUseNumber)
EnableJsonDecoderUseNumber() EnableJsonDecoderUseNumber()
assert.True(t, binding.EnableDecoderUseNumber) assert.True(t, binding.EnableDecoderUseNumber)
} }
func TestEnableJsonDecoderDisallowUnknownFields(t *testing.T) {
assert.False(t, binding.EnableDecoderDisallowUnknownFields)
EnableJsonDecoderDisallowUnknownFields()
assert.True(t, binding.EnableDecoderDisallowUnknownFields)
}

57
path.go
View File

@ -19,13 +19,17 @@ package gin
// //
// If the result of this process is an empty string, "/" is returned. // If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string { func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/" // Turn empty string into "/"
if p == "" { if p == "" {
return "/" return "/"
} }
// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)
n := len(p) n := len(p)
var buf []byte
// Invariants: // Invariants:
// reading from path; r is index of next byte to process. // reading from path; r is index of next byte to process.
@ -37,15 +41,21 @@ func cleanPath(p string) string {
if p[0] != '/' { if p[0] != '/' {
r = 0 r = 0
buf = make([]byte, n+1)
if n+1 > stackBufSize {
buf = make([]byte, n+1)
} else {
buf = buf[:n+1]
}
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 1 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n { for r < n {
switch { switch {
@ -69,7 +79,7 @@ func cleanPath(p string) string {
// can backtrack // can backtrack
w-- w--
if buf == nil { if len(buf) == 0 {
for w > 1 && p[w] != '/' { for w > 1 && p[w] != '/' {
w-- w--
} }
@ -81,14 +91,14 @@ func cleanPath(p string) string {
} }
default: default:
// real path element. // Real path element.
// add slash if needed // Add slash if needed
if w > 1 { if w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
// copy element // Copy element
for r < n && p[r] != '/' { for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r]) bufApp(&buf, p, w, p[r])
w++ w++
@ -97,27 +107,44 @@ func cleanPath(p string) string {
} }
} }
// re-append trailing slash // Re-append trailing slash
if trailing && w > 1 { if trailing && w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
if buf == nil { // If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise return a new string from the buffer.
if len(buf) == 0 {
return p[:w] return p[:w]
} }
return string(buf[:w]) return string(buf[:w])
} }
// internal helper to lazily create a buffer if necessary. // Internal helper to lazily create a buffer if necessary.
// Calls to this function get inlined.
func bufApp(buf *[]byte, s string, w int, c byte) { func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil { b := *buf
if len(b) == 0 {
// No modification of the original string so far.
// If the next character is the same as in the original string, we do
// not yet have to allocate a buffer.
if s[w] == c { if s[w] == c {
return return
} }
*buf = make([]byte, len(s)) // Otherwise use either the stack buffer, if it is large enough, or
copy(*buf, s[:w]) // allocate a new buffer on the heap, and copy all previous characters.
length := len(s)
if length > cap(b) {
*buf = make([]byte, length)
} else {
*buf = (*buf)[:length]
}
b = *buf
copy(b, s[:w])
} }
(*buf)[w] = c b[w] = c
} }

View File

@ -6,15 +6,17 @@
package gin package gin
import ( import (
"runtime" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var cleanTests = []struct { type cleanPathTest struct {
path, result string path, result string
}{ }
var cleanTests = []cleanPathTest{
// Already clean // Already clean
{"/", "/"}, {"/", "/"},
{"/abc", "/abc"}, {"/abc", "/abc"},
@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping malloc count in short mode") t.Skip("skipping malloc count in short mode")
} }
if runtime.GOMAXPROCS(0) > 1 {
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
return
}
for _, test := range cleanTests { for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0) assert.EqualValues(t, allocs, 0)
} }
} }
func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}
func genLongPaths() (testPaths []cleanPathTest) {
for i := 1; i <= 1234; i++ {
ss := strings.Repeat("a", i)
correctPath := "/" + ss
testPaths = append(testPaths, cleanPathTest{
path: correctPath,
result: correctPath,
}, cleanPathTest{
path: ss,
result: correctPath,
}, cleanPathTest{
path: "//" + ss,
result: correctPath,
}, cleanPathTest{
path: "/" + ss + "/b/..",
result: correctPath,
})
}
return
}
func TestPathCleanLong(t *testing.T) {
cleanTests := genLongPaths()
for _, test := range cleanTests {
assert.Equal(t, test.result, cleanPath(test.path))
assert.Equal(t, test.result, cleanPath(test.result))
}
}
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}

View File

@ -26,13 +26,29 @@ var (
slash = []byte("/") slash = []byte("/")
) )
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err interface{})
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc { func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter) return RecoveryWithWriter(DefaultErrorWriter)
} }
// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one. // RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc { func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
if len(recovery) > 0 {
return CustomRecoveryWithWriter(out, recovery[0])
}
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
var logger *log.Logger var logger *log.Logger
if out != nil { if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
@ -60,23 +76,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
headers[idx] = current[0] + ": *" headers[idx] = current[0] + ": *"
} }
} }
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset) logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) timeFormat(time.Now()), headersToStr, err, stack, reset)
} else { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset) timeFormat(time.Now()), err, stack, reset)
} }
} }
// If the connection is dead, we can't write a status to it.
if brokenPipe { if brokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck c.Error(err.(error)) // nolint: errcheck
c.Abort() c.Abort()
} else { } else {
c.AbortWithStatus(http.StatusInternalServerError) handle(c, err)
} }
} }
}() }()
@ -84,6 +100,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
} }
} }
func defaultHandleRecovery(c *Context, err interface{}) {
c.AbortWithStatus(http.StatusInternalServerError)
}
// stack returns a nicely formatted stack frame, skipping skip frames. // stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte { func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data buf := new(bytes.Buffer) // the returned data
@ -145,7 +165,7 @@ func function(pc uintptr) []byte {
return name return name
} }
// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string { func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05") return t.Format("2006/01/02 - 15:04:05")
return timeString
} }

View File

@ -62,7 +62,7 @@ func TestPanicInHandler(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler") assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery") assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request // Debug mode prints the request
@ -92,14 +92,14 @@ func TestPanicWithAbort(t *testing.T) {
func TestSource(t *testing.T) { func TestSource(t *testing.T) {
bs := source(nil, 0) bs := source(nil, 0)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
in := [][]byte{ in := [][]byte{
[]byte("Hello world."), []byte("Hello world."),
[]byte("Hi, gin.."), []byte("Hi, gin.."),
} }
bs = source(in, 10) bs = source(in, 10)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
bs = source(in, 1) bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs) assert.Equal(t, []byte("Hello world."), bs)
@ -107,7 +107,7 @@ func TestSource(t *testing.T) {
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
bs := function(1) bs := function(1)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
} }
// TestPanicWithBrokenPipe asserts that recovery specifically handles // TestPanicWithBrokenPipe asserts that recovery specifically handles
@ -144,3 +144,107 @@ func TestPanicWithBrokenPipe(t *testing.T) {
}) })
} }
} }
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(bytes.Buffer)
buffer := new(bytes.Buffer)
router := New()
handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
func TestCustomRecovery(t *testing.T) {
errBuffer := new(bytes.Buffer)
buffer := new(bytes.Buffer)
router := New()
DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(CustomRecovery(handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
errBuffer := new(bytes.Buffer)
buffer := new(bytes.Buffer)
router := New()
DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}

View File

@ -10,6 +10,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -40,17 +41,16 @@ type AsciiJSON struct {
Data interface{} Data interface{}
} }
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
// PureJSON contains the given interface object. // PureJSON contains the given interface object.
type PureJSON struct { type PureJSON struct {
Data interface{} Data interface{}
} }
var jsonContentType = []string{"application/json; charset=utf-8"} var (
var jsonpContentType = []string{"application/javascript; charset=utf-8"} jsonContentType = []string{"application/json; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"} jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonAsciiContentType = []string{"application/json"}
)
// Render (JSON) writes data with custom ContentType. // Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {
@ -100,8 +100,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return err return err
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
_, err = w.Write([]byte(r.Prefix)) bytesconv.StringToBytes("]")) {
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil { if err != nil {
return err return err
} }
@ -129,11 +130,11 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
_, err = w.Write([]byte(callback)) _, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte("(")) _, err = w.Write(bytesconv.StringToBytes("("))
if err != nil { if err != nil {
return err return err
} }
@ -141,7 +142,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte(")")) _, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil { if err != nil {
return err return err
} }
@ -163,7 +164,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for _, r := range string(ret) { for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r) cvt := string(r)
if r >= 128 { if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r)) cvt = fmt.Sprintf("\\u%04x", int64(r))

View File

@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package render package render
import ( import (
@ -10,6 +13,12 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
// Check interface implemented here to support go build tag nomsgpack.
// See: https://github.com/gin-gonic/gin/pull/1852/
var (
_ Render = MsgPack{}
)
// MsgPack contains the given interface object. // MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data interface{}

View File

@ -21,7 +21,12 @@ type Reader struct {
// Render (Reader) writes data with custom ContentType and headers. // Render (Reader) writes data with custom ContentType and headers.
func (r Reader) Render(w http.ResponseWriter) (err error) { func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) if r.ContentLength >= 0 {
if r.Headers == nil {
r.Headers = map[string]string{}
}
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers) r.writeHeaders(w, r.Headers)
_, err = io.Copy(w, r.Reader) _, err = io.Copy(w, r.Reader)
return return

23
render/reader_test.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestReaderRenderNoHeaders(t *testing.T) {
content := "test"
r := Reader{
ContentLength: int64(len(content)),
Reader: strings.NewReader(content),
}
err := r.Render(httptest.NewRecorder())
require.NoError(t, err)
}

View File

@ -27,7 +27,6 @@ var (
_ HTMLRender = HTMLDebug{} _ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{} _ HTMLRender = HTMLProduction{}
_ Render = YAML{} _ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{} _ Render = Reader{}
_ Render = AsciiJSON{} _ Render = AsciiJSON{}
_ Render = ProtoBuf{} _ Render = ProtoBuf{}

View File

@ -0,0 +1,44 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package render
import (
"bytes"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -5,7 +5,6 @@
package render package render
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"html/template" "html/template"
@ -17,7 +16,6 @@ import (
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
@ -25,30 +23,6 @@ import (
// TODO unit tests // TODO unit tests
// test errors // test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderJSON(t *testing.T) { func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]interface{}{
@ -146,7 +120,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1) err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1) assert.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String()) assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
@ -158,7 +132,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err2 := (JsonpJSON{"x", datas}).Render(w2) err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2) assert.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String()) assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
} }
@ -347,7 +321,20 @@ func TestRenderRedirect(t *testing.T) {
} }
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) }) assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w)
assert.NoError(t, err)
})
data3 := Redirect{
Code: http.StatusCreated,
Request: req,
Location: "/new/location",
}
w = httptest.NewRecorder()
err = data3.Render(w)
assert.NoError(t, err)
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -498,3 +485,26 @@ func TestRenderReader(t *testing.T) {
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
} }
func TestRenderReaderNoContentLength(t *testing.T) {
w := httptest.NewRecorder()
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
headers["x-request-id"] = "requestId"
err := (Reader{
ContentLength: -1,
ContentType: "image/png",
Reader: strings.NewReader(body),
Headers: headers,
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header())
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}

View File

@ -6,8 +6,9 @@ package render
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// String contains the given interface object slice and its format. // String contains the given interface object slice and its format.
@ -35,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err
_, err = fmt.Fprintf(w, format, data...) _, err = fmt.Fprintf(w, format, data...)
return return
} }
_, err = io.WriteString(w, format) _, err = w.Write(bytesconv.StringToBytes(format))
return return
} }

View File

@ -29,38 +29,38 @@ func init() {
} }
func TestResponseWriterReset(t *testing.T) { func TestResponseWriterReset(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
var w ResponseWriter = writer var w ResponseWriter = writer
writer.reset(testWritter) writer.reset(testWriter)
assert.Equal(t, -1, writer.size) assert.Equal(t, -1, writer.size)
assert.Equal(t, http.StatusOK, writer.status) assert.Equal(t, http.StatusOK, writer.status)
assert.Equal(t, testWritter, writer.ResponseWriter) assert.Equal(t, testWriter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size()) assert.Equal(t, -1, w.Size())
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written()) assert.False(t, w.Written())
} }
func TestResponseWriterWriteHeader(t *testing.T) { func TestResponseWriterWriteHeader(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices) w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written()) assert.False(t, w.Written())
assert.Equal(t, http.StatusMultipleChoices, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code) assert.NotEqual(t, http.StatusMultipleChoices, testWriter.Code)
w.WriteHeader(-1) w.WriteHeader(-1)
assert.Equal(t, http.StatusMultipleChoices, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
} }
func TestResponseWriterWriteHeadersNow(t *testing.T) { func TestResponseWriterWriteHeadersNow(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices) w.WriteHeader(http.StatusMultipleChoices)
@ -68,7 +68,7 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
assert.True(t, w.Written()) assert.True(t, w.Written())
assert.Equal(t, 0, w.Size()) assert.Equal(t, 0, w.Size())
assert.Equal(t, http.StatusMultipleChoices, testWritter.Code) assert.Equal(t, http.StatusMultipleChoices, testWriter.Code)
writer.size = 10 writer.size = 10
w.WriteHeaderNow() w.WriteHeaderNow()
@ -76,30 +76,30 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
} }
func TestResponseWriterWrite(t *testing.T) { func TestResponseWriterWrite(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
n, err := w.Write([]byte("hola")) n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n) assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size()) assert.Equal(t, 4, w.Size())
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWritter.Code) assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWritter.Body.String()) assert.Equal(t, "hola", testWriter.Body.String())
assert.NoError(t, err) assert.NoError(t, err)
n, err = w.Write([]byte(" adios")) n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n) assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size()) assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWritter.Body.String()) assert.Equal(t, "hola adios", testWriter.Body.String())
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResponseWriterHijack(t *testing.T) { func TestResponseWriterHijack(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
assert.Panics(t, func() { assert.Panics(t, func() {

View File

@ -11,6 +11,11 @@ import (
"strings" "strings"
) )
var (
// reg match english letters for http method name
regEnLetter = regexp.MustCompile("^[A-Z]+$")
)
// IRouter defines all router handle interface includes single and group router. // IRouter defines all router handle interface includes single and group router.
type IRouter interface { type IRouter interface {
IRoutes IRoutes
@ -87,7 +92,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// frequently used, non-standardized or custom methods (e.g. for internal // frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy). // communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid") panic("http method " + httpMethod + " is not valid")
} }
return group.handle(httpMethod, relativePath, handlers) return group.handle(httpMethod, relativePath, handlers)
@ -95,51 +100,51 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
// POST is a shortcut for router.Handle("POST", path, handle). // POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("POST", relativePath, handlers) return group.handle(http.MethodPost, relativePath, handlers)
} }
// GET is a shortcut for router.Handle("GET", path, handle). // GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers) return group.handle(http.MethodGet, relativePath, handlers)
} }
// DELETE is a shortcut for router.Handle("DELETE", path, handle). // DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("DELETE", relativePath, handlers) return group.handle(http.MethodDelete, relativePath, handlers)
} }
// PATCH is a shortcut for router.Handle("PATCH", path, handle). // PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("PATCH", relativePath, handlers) return group.handle(http.MethodPatch, relativePath, handlers)
} }
// PUT is a shortcut for router.Handle("PUT", path, handle). // PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("PUT", relativePath, handlers) return group.handle(http.MethodPut, relativePath, handlers)
} }
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle). // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("OPTIONS", relativePath, handlers) return group.handle(http.MethodOptions, relativePath, handlers)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handle). // HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("HEAD", relativePath, handlers) return group.handle(http.MethodHead, relativePath, handlers)
} }
// Any registers a route that matches all the HTTP methods. // Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
group.handle("GET", relativePath, handlers) group.handle(http.MethodGet, relativePath, handlers)
group.handle("POST", relativePath, handlers) group.handle(http.MethodPost, relativePath, handlers)
group.handle("PUT", relativePath, handlers) group.handle(http.MethodPut, relativePath, handlers)
group.handle("PATCH", relativePath, handlers) group.handle(http.MethodPatch, relativePath, handlers)
group.handle("HEAD", relativePath, handlers) group.handle(http.MethodHead, relativePath, handlers)
group.handle("OPTIONS", relativePath, handlers) group.handle(http.MethodOptions, relativePath, handlers)
group.handle("DELETE", relativePath, handlers) group.handle(http.MethodDelete, relativePath, handlers)
group.handle("CONNECT", relativePath, handlers) group.handle(http.MethodConnect, relativePath, handlers)
group.handle("TRACE", relativePath, handlers) group.handle(http.MethodTrace, relativePath, handlers)
return group.returnObj() return group.returnObj()
} }
@ -187,19 +192,21 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) { return func(c *Context) {
if _, nolisting := fs.(*onlyfilesFS); nolisting { if _, noListing := fs.(*onlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
} }
file := c.Param("filepath") file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it // Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil { f, err := fs.Open(file)
if err != nil {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
c.handlers = group.engine.noRoute c.handlers = group.engine.noRoute
// Reset index // Reset index
c.index = -1 c.index = -1
return return
} }
f.Close()
fileServer.ServeHTTP(c.Writer, c.Request) fileServer.ServeHTTP(c.Writer, c.Request)
} }
@ -207,9 +214,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers) finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { assert1(finalSize < int(abortIndex), "too many handlers")
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize) mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers) copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers) copy(mergedHandlers[len(group.Handlers):], handlers)

View File

@ -33,13 +33,13 @@ func TestRouterGroupBasic(t *testing.T) {
} }
func TestRouterGroupBasicHandle(t *testing.T) { func TestRouterGroupBasicHandle(t *testing.T) {
performRequestInGroup(t, "GET") performRequestInGroup(t, http.MethodGet)
performRequestInGroup(t, "POST") performRequestInGroup(t, http.MethodPost)
performRequestInGroup(t, "PUT") performRequestInGroup(t, http.MethodPut)
performRequestInGroup(t, "PATCH") performRequestInGroup(t, http.MethodPatch)
performRequestInGroup(t, "DELETE") performRequestInGroup(t, http.MethodDelete)
performRequestInGroup(t, "HEAD") performRequestInGroup(t, http.MethodHead)
performRequestInGroup(t, "OPTIONS") performRequestInGroup(t, http.MethodOptions)
} }
func performRequestInGroup(t *testing.T, method string) { func performRequestInGroup(t *testing.T, method string) {
@ -55,25 +55,25 @@ func performRequestInGroup(t *testing.T, method string) {
} }
switch method { switch method {
case "GET": case http.MethodGet:
v1.GET("/test", handler) v1.GET("/test", handler)
login.GET("/test", handler) login.GET("/test", handler)
case "POST": case http.MethodPost:
v1.POST("/test", handler) v1.POST("/test", handler)
login.POST("/test", handler) login.POST("/test", handler)
case "PUT": case http.MethodPut:
v1.PUT("/test", handler) v1.PUT("/test", handler)
login.PUT("/test", handler) login.PUT("/test", handler)
case "PATCH": case http.MethodPatch:
v1.PATCH("/test", handler) v1.PATCH("/test", handler)
login.PATCH("/test", handler) login.PATCH("/test", handler)
case "DELETE": case http.MethodDelete:
v1.DELETE("/test", handler) v1.DELETE("/test", handler)
login.DELETE("/test", handler) login.DELETE("/test", handler)
case "HEAD": case http.MethodHead:
v1.HEAD("/test", handler) v1.HEAD("/test", handler)
login.HEAD("/test", handler) login.HEAD("/test", handler)
case "OPTIONS": case http.MethodOptions:
v1.OPTIONS("/test", handler) v1.OPTIONS("/test", handler)
login.OPTIONS("/test", handler) login.OPTIONS("/test", handler)
default: default:
@ -112,15 +112,19 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
} }
func TestRouterGroupTooManyHandlers(t *testing.T) { func TestRouterGroupTooManyHandlers(t *testing.T) {
const (
panicValue = "too many handlers"
maximumCnt = abortIndex
)
router := New() router := New()
handlers1 := make([]HandlerFunc, 40) handlers1 := make([]HandlerFunc, maximumCnt-1)
router.Use(handlers1...) router.Use(handlers1...)
handlers2 := make([]HandlerFunc, 26) handlers2 := make([]HandlerFunc, maximumCnt+1)
assert.Panics(t, func() { assert.PanicsWithValue(t, panicValue, func() {
router.Use(handlers2...) router.Use(handlers2...)
}) })
assert.Panics(t, func() { assert.PanicsWithValue(t, panicValue, func() {
router.GET("/", handlers2...) router.GET("/", handlers2...)
}) })
} }
@ -128,7 +132,7 @@ func TestRouterGroupTooManyHandlers(t *testing.T) {
func TestRouterGroupBadMethod(t *testing.T) { func TestRouterGroupBadMethod(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
router.Handle("get", "/") router.Handle(http.MethodGet, "/")
}) })
assert.Panics(t, func() { assert.Panics(t, func() {
router.Handle(" GET", "/") router.Handle(" GET", "/")
@ -162,7 +166,7 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
handler := func(c *Context) {} handler := func(c *Context) {}
assert.Equal(t, r, r.Use(handler)) assert.Equal(t, r, r.Use(handler))
assert.Equal(t, r, r.Handle("GET", "/handler", handler)) assert.Equal(t, r, r.Handle(http.MethodGet, "/handler", handler))
assert.Equal(t, r, r.Any("/any", handler)) assert.Equal(t, r, r.Any("/any", handler))
assert.Equal(t, r, r.GET("/", handler)) assert.Equal(t, r, r.GET("/", handler))
assert.Equal(t, r, r.POST("/", handler)) assert.Equal(t, r, r.POST("/", handler))

View File

@ -70,10 +70,10 @@ func testRouteNotOK2(method string, t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
var methodRoute string var methodRoute string
if method == "POST" { if method == http.MethodPost {
methodRoute = "GET" methodRoute = http.MethodGet
} else { } else {
methodRoute = "POST" methodRoute = http.MethodPost
} }
router.Handle(methodRoute, "/test", func(c *Context) { router.Handle(methodRoute, "/test", func(c *Context) {
passed = true passed = true
@ -99,46 +99,46 @@ func TestRouterMethod(t *testing.T) {
c.String(http.StatusOK, "sup3") c.String(http.StatusOK, "sup3")
}) })
w := performRequest(router, "PUT", "/hey") w := performRequest(router, http.MethodPut, "/hey")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "called", w.Body.String()) assert.Equal(t, "called", w.Body.String())
} }
func TestRouterGroupRouteOK(t *testing.T) { func TestRouterGroupRouteOK(t *testing.T) {
testRouteOK("GET", t) testRouteOK(http.MethodGet, t)
testRouteOK("POST", t) testRouteOK(http.MethodPost, t)
testRouteOK("PUT", t) testRouteOK(http.MethodPut, t)
testRouteOK("PATCH", t) testRouteOK(http.MethodPatch, t)
testRouteOK("HEAD", t) testRouteOK(http.MethodHead, t)
testRouteOK("OPTIONS", t) testRouteOK(http.MethodOptions, t)
testRouteOK("DELETE", t) testRouteOK(http.MethodDelete, t)
testRouteOK("CONNECT", t) testRouteOK(http.MethodConnect, t)
testRouteOK("TRACE", t) testRouteOK(http.MethodTrace, t)
} }
func TestRouteNotOK(t *testing.T) { func TestRouteNotOK(t *testing.T) {
testRouteNotOK("GET", t) testRouteNotOK(http.MethodGet, t)
testRouteNotOK("POST", t) testRouteNotOK(http.MethodPost, t)
testRouteNotOK("PUT", t) testRouteNotOK(http.MethodPut, t)
testRouteNotOK("PATCH", t) testRouteNotOK(http.MethodPatch, t)
testRouteNotOK("HEAD", t) testRouteNotOK(http.MethodHead, t)
testRouteNotOK("OPTIONS", t) testRouteNotOK(http.MethodOptions, t)
testRouteNotOK("DELETE", t) testRouteNotOK(http.MethodDelete, t)
testRouteNotOK("CONNECT", t) testRouteNotOK(http.MethodConnect, t)
testRouteNotOK("TRACE", t) testRouteNotOK(http.MethodTrace, t)
} }
func TestRouteNotOK2(t *testing.T) { func TestRouteNotOK2(t *testing.T) {
testRouteNotOK2("GET", t) testRouteNotOK2(http.MethodGet, t)
testRouteNotOK2("POST", t) testRouteNotOK2(http.MethodPost, t)
testRouteNotOK2("PUT", t) testRouteNotOK2(http.MethodPut, t)
testRouteNotOK2("PATCH", t) testRouteNotOK2(http.MethodPatch, t)
testRouteNotOK2("HEAD", t) testRouteNotOK2(http.MethodHead, t)
testRouteNotOK2("OPTIONS", t) testRouteNotOK2(http.MethodOptions, t)
testRouteNotOK2("DELETE", t) testRouteNotOK2(http.MethodDelete, t)
testRouteNotOK2("CONNECT", t) testRouteNotOK2(http.MethodConnect, t)
testRouteNotOK2("TRACE", t) testRouteNotOK2(http.MethodTrace, t)
} }
func TestRouteRedirectTrailingSlash(t *testing.T) { func TestRouteRedirectTrailingSlash(t *testing.T) {
@ -150,50 +150,50 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.POST("/path3", func(c *Context) {}) router.POST("/path3", func(c *Context) {})
router.PUT("/path4/", func(c *Context) {}) router.PUT("/path4/", func(c *Context) {})
w := performRequest(router, "GET", "/path/") w := performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/path2/", w.Header().Get("Location")) assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, "/path3", w.Header().Get("Location")) assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, "/path4/", w.Header().Get("Location")) assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2/") w = performRequest(router, http.MethodGet, "/path2/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "PUT", "/path4/") w = performRequest(router, http.MethodPut, "/path4/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) w = performRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
w = performRequest(router, "GET", "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) w = performRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/") w = performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
@ -207,19 +207,19 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/PATH3", func(c *Context) {}) router.POST("/PATH3", func(c *Context) {})
router.POST("/Path4/", func(c *Context) {}) router.POST("/Path4/", func(c *Context) {})
w := performRequest(router, "GET", "/PATH") w := performRequest(router, http.MethodGet, "/PATH")
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/Path2", w.Header().Get("Location")) assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, "/PATH3", w.Header().Get("Location")) assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "POST", "/path4") w = performRequest(router, http.MethodPost, "/path4")
assert.Equal(t, "/Path4/", w.Header().Get("Location")) assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
} }
@ -238,7 +238,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))
@ -249,7 +248,7 @@ func TestRouteParamsByName(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
}) })
w := performRequest(router, "GET", "/test/john/smith/is/super/great") w := performRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name) assert.Equal(t, "john", name)
@ -263,6 +262,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
lastName := "" lastName := ""
wild := "" wild := ""
router := New() router := New()
router.RemoveExtraSlash = true
router.GET("/test/:name/:last_name/*wild", func(c *Context) { router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name") name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name") lastName = c.Params.ByName("last_name")
@ -271,7 +271,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))
@ -282,7 +281,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
}) })
w := performRequest(router, "GET", "//test//john//smith//is//super//great") w := performRequest(router, http.MethodGet, "//test//john//smith//is//super//great")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name) assert.Equal(t, "john", name)
@ -310,16 +309,16 @@ func TestRouteStaticFile(t *testing.T) {
router.Static("/using_static", dir) router.Static("/using_static", dir)
router.StaticFile("/result", f.Name()) router.StaticFile("/result", f.Name())
w := performRequest(router, "GET", "/using_static/"+filename) w := performRequest(router, http.MethodGet, "/using_static/"+filename)
w2 := performRequest(router, "GET", "/result") w2 := performRequest(router, http.MethodGet, "/result")
assert.Equal(t, w, w2) assert.Equal(t, w, w2)
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String()) assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename) w3 := performRequest(router, http.MethodHead, "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result") w4 := performRequest(router, http.MethodHead, "/result")
assert.Equal(t, w3, w4) assert.Equal(t, w3, w4)
assert.Equal(t, http.StatusOK, w3.Code) assert.Equal(t, http.StatusOK, w3.Code)
@ -330,7 +329,7 @@ func TestRouteStaticListingDir(t *testing.T) {
router := New() router := New()
router.StaticFS("/", Dir("./", true)) router.StaticFS("/", Dir("./", true))
w := performRequest(router, "GET", "/") w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go") assert.Contains(t, w.Body.String(), "gin.go")
@ -342,7 +341,7 @@ func TestRouteStaticNoListing(t *testing.T) {
router := New() router := New()
router.Static("/", "./") router.Static("/", "./")
w := performRequest(router, "GET", "/") w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go") assert.NotContains(t, w.Body.String(), "gin.go")
@ -357,11 +356,13 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
}) })
static.Static("/", "./") static.Static("/", "./")
w := performRequest(router, "GET", "/gin.go") w := performRequest(router, http.MethodGet, "/gin.go")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@ -371,13 +372,13 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "responseText", w.Body.String()) assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, http.StatusTeapot, w.Code) assert.Equal(t, http.StatusTeapot, w.Code)
} }
@ -386,9 +387,9 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
// add one methodTree to trees // add one methodTree to trees
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
router.GET("/path2", func(c *Context) {}) router.GET("/path2", func(c *Context) {})
w := performRequest(router, "POST", "/path2") w := performRequest(router, http.MethodPost, "/path2")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
@ -396,17 +397,40 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "404 page not found", w.Body.String()) assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
router := New()
router.RemoveExtraSlash = true
router.GET("/path", func(c *Context) {})
router.GET("/", func(c *Context) {})
testRoutes := []struct {
route string
code int
location string
}{
{"/../path", http.StatusOK, ""}, // CleanPath
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
}
}
}
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
router := New() router := New()
router.RedirectFixedPath = true router.RedirectFixedPath = true
@ -419,17 +443,17 @@ func TestRouterNotFound(t *testing.T) {
code int code int
location string location string
}{ }{
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/ {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/ {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
{"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
{"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
{"/../path", http.StatusOK, ""}, // CleanPath {"/../path", http.StatusMovedPermanently, "/path"}, // Without CleanPath
{"/nope", http.StatusNotFound, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route) w := performRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location"))) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
@ -442,20 +466,20 @@ func TestRouterNotFound(t *testing.T) {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
notFound = true notFound = true
}) })
w := performRequest(router, "GET", "/nope") w := performRequest(router, http.MethodGet, "/nope")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound) assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301) // Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {}) router.PATCH("/path", func(c *Context) {})
w = performRequest(router, "PATCH", "/path/") w = performRequest(router, http.MethodPatch, "/path/")
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header())) assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists // Test special case where no node for the prefix "/" exists
router = New() router = New()
router.GET("/a", func(c *Context) {}) router.GET("/a", func(c *Context) {})
w = performRequest(router, "GET", "/") w = performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
@ -466,10 +490,10 @@ func TestRouterStaticFSNotFound(t *testing.T) {
c.String(404, "non existent") c.String(404, "non existent")
}) })
w := performRequest(router, "GET", "/nonexistent") w := performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String()) assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, "HEAD", "/nonexistent") w = performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String()) assert.Equal(t, "non existent", w.Body.String())
} }
@ -479,7 +503,7 @@ func TestRouterStaticFSFileNotFound(t *testing.T) {
router.StaticFS("/", http.FileSystem(http.Dir("."))) router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
performRequest(router, "GET", "/nonexistent") performRequest(router, http.MethodGet, "/nonexistent")
}) })
} }
@ -490,17 +514,17 @@ func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) {
// Middleware must be called just only once by per request. // Middleware must be called just only once by per request.
middlewareCalledNum := 0 middlewareCalledNum := 0
router.Use(func(c *Context) { router.Use(func(c *Context) {
middlewareCalledNum += 1 middlewareCalledNum++
}) })
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
// First access // First access
performRequest(router, "GET", "/nonexistent") performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, 1, middlewareCalledNum) assert.Equal(t, 1, middlewareCalledNum)
// Second access // Second access
performRequest(router, "HEAD", "/nonexistent") performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, 2, middlewareCalledNum) assert.Equal(t, 2, middlewareCalledNum)
} }
@ -519,7 +543,7 @@ func TestRouteRawPath(t *testing.T) {
assert.Equal(t, "222", num) assert.Equal(t, "222", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222") w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -539,7 +563,7 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
assert.Equal(t, "333", num) assert.Equal(t, "333", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333") w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -550,7 +574,50 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
c.Next() c.Next()
}) })
w := performRequest(route, "GET", "/NotFound") w := performRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, 421, w.Code) assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestRouteContextHoldsFullPath(t *testing.T) {
router := New()
// Test routes
routes := []string{
"/simple",
"/project/:name",
"/",
"/news/home",
"/news",
"/simple-two/one",
"/simple-two/one-two",
"/project/:name/build/*params",
"/project/:name/bui",
"/user/:id/status",
"/user/:id",
"/user/:id/profile",
}
for _, route := range routes {
actualRoute := route
router.GET(route, func(c *Context) {
// For each defined route context should contain its full path
assert.Equal(t, actualRoute, c.FullPath())
c.AbortWithStatus(http.StatusOK)
})
}
for _, route := range routes {
w := performRequest(router, http.MethodGet, route)
assert.Equal(t, http.StatusOK, w.Code)
}
// Test not found
router.Use(func(c *Context) {
// For not found routes full path is empty
assert.Equal(t, "", c.FullPath())
})
w := performRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code)
}

924
tree.go

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,11 @@ type testRequests []struct {
ps Params ps Params
} }
func getParams() *Params {
ps := make(Params, 0, 20)
return &ps
}
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) { func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false unescape := false
if len(unescapes) >= 1 { if len(unescapes) >= 1 {
@ -35,24 +40,27 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
} }
for _, request := range requests { for _, request := range requests {
handler, ps, _ := tree.getValue(request.path, nil, unescape) value := tree.getValue(request.path, getParams(), unescape)
if handler == nil { if value.handlers == nil {
if !request.nilHandler { if !request.nilHandler {
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
} }
} else if request.nilHandler { } else if request.nilHandler {
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
} else { } else {
handler[0](nil) value.handlers[0](nil)
if fakeHandlerValue != request.route { if fakeHandlerValue != request.route {
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
} }
} }
if !reflect.DeepEqual(ps, request.ps) { if value.params != nil {
t.Errorf("Params mismatch for route '%s'", request.path) if !reflect.DeepEqual(*value.params, request.ps) {
t.Errorf("Params mismatch for route '%s'", request.path)
}
} }
} }
} }
@ -76,33 +84,11 @@ func checkPriorities(t *testing.T, n *node) uint32 {
return prio return prio
} }
func checkMaxParams(t *testing.T, n *node) uint8 {
var maxParams uint8
for i := range n.children {
params := checkMaxParams(t, n.children[i])
if params > maxParams {
maxParams = params
}
}
if n.nType > root && !n.wildChild {
maxParams++
}
if n.maxParams != maxParams {
t.Errorf(
"maxParams mismatch for node '%s': is %d, should be %d",
n.path, n.maxParams, maxParams,
)
}
return maxParams
}
func TestCountParams(t *testing.T) { func TestCountParams(t *testing.T) {
if countParams("/path/:param1/static/*catch-all") != 2 { if countParams("/path/:param1/static/*catch-all") != 2 {
t.Fail() t.Fail()
} }
if countParams(strings.Repeat("/:param", 256)) != 255 { if countParams(strings.Repeat("/:param", 256)) != 256 {
t.Fail() t.Fail()
} }
} }
@ -142,7 +128,6 @@ func TestTreeAddAndGet(t *testing.T) {
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func TestTreeWildcard(t *testing.T) { func TestTreeWildcard(t *testing.T) {
@ -150,11 +135,16 @@ func TestTreeWildcard(t *testing.T) {
routes := [...]string{ routes := [...]string{
"/", "/",
"/cmd/:tool/:sub",
"/cmd/:tool/", "/cmd/:tool/",
"/cmd/:tool/:sub",
"/cmd/whoami",
"/cmd/whoami/root",
"/cmd/whoami/root/",
"/src/*filepath", "/src/*filepath",
"/search/", "/search/",
"/search/:query", "/search/:query",
"/search/gin-gonic",
"/search/google",
"/user_:name", "/user_:name",
"/user_:name/about", "/user_:name/about",
"/files/:dir/*filepath", "/files/:dir/*filepath",
@ -163,6 +153,38 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html", "/doc/go1.html",
"/info/:user/public", "/info/:user/public",
"/info/:user/project/:project", "/info/:user/project/:project",
"/info/:user/project/golang",
"/aa/*xx",
"/ab/*xx",
"/:cc",
"/:cc/cc",
"/:cc/:dd/ee",
"/:cc/:dd/:ee/ff",
"/:cc/:dd/:ee/:ff/gg",
"/:cc/:dd/:ee/:ff/:gg/hh",
"/get/test/abc/",
"/get/:param/abc/",
"/something/:paramname/thirdthing",
"/something/secondthing/test",
"/get/abc",
"/get/:param",
"/get/abc/123abc",
"/get/abc/:param",
"/get/abc/123abc/xxx8",
"/get/abc/123abc/:param",
"/get/abc/123abc/xxx8/1234",
"/get/abc/123abc/xxx8/:param",
"/get/abc/123abc/xxx8/1234/ffas",
"/get/abc/123abc/xxx8/1234/:param",
"/get/abc/123abc/xxx8/1234/kkdd/12c",
"/get/abc/123abc/xxx8/1234/kkdd/:param",
"/get/abc/:param/test",
"/get/abc/123abd/:param",
"/get/abc/123abddd/:param",
"/get/abc/123/:param",
"/get/abc/123abg/:param",
"/get/abc/123abf/:param",
"/get/abc/123abfff/:param",
} }
for _, route := range routes { for _, route := range routes {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
@ -170,23 +192,122 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}}, {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/whoami", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/cmd/whoami", nil},
{"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}}, {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil}, {"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
{"/search/google", false, "/search/google", nil},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}}, {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}}, {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
// * Error with argument being intercepted
// new PR handle (/all /all/cc /a/cc)
// fix PR: https://github.com/gin-gonic/gin/pull/2796
{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
{"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
{"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
{"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
{"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
{"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
{"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
{"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
{"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
{"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
{"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
{"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
{"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
{"/get/test/abc/", false, "/get/test/abc/", nil},
{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
{"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
{"/something/secondthing/test", false, "/something/secondthing/test", nil},
{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
{"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
{"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
{"/get/abc", false, "/get/abc", nil},
{"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
{"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
{"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
{"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
{"/get/abc/123abc", false, "/get/abc/123abc", nil},
{"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
{"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
{"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
{"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
{"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
{"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
{"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
{"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
{"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
{"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
{"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
{"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
{"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
{"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
{"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
{"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
{"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
{"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
{"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
{"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
{"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
{"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
{"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func TestUnescapeParameters(t *testing.T) { func TestUnescapeParameters(t *testing.T) {
@ -224,7 +345,6 @@ func TestUnescapeParameters(t *testing.T) {
}, unescape) }, unescape)
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func catchPanic(testFunc func()) (recv interface{}) { func catchPanic(testFunc func()) (recv interface{}) {
@ -262,20 +382,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) { func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{ routes := []testRoute{
{"/cmd/:tool/:sub", false}, {"/cmd/:tool/:sub", false},
{"/cmd/vet", true}, {"/cmd/vet", false},
{"/foo/bar", false},
{"/foo/:name", false},
{"/foo/:names", true},
{"/cmd/*path", true},
{"/cmd/:badvar", true},
{"/cmd/:tool/names", false},
{"/cmd/:tool/:badsub/details", true},
{"/src/*filepath", false}, {"/src/*filepath", false},
{"/src/:file", true},
{"/src/static.json", true},
{"/src/*filepathx", true}, {"/src/*filepathx", true},
{"/src/", true}, {"/src/", true},
{"/src/foo/bar", true},
{"/src1/", false}, {"/src1/", false},
{"/src1/*filepath", true}, {"/src1/*filepath", true},
{"/src2*filepath", true}, {"/src2*filepath", true},
{"/src2/*filepath", false},
{"/search/:query", false}, {"/search/:query", false},
{"/search/invalid", true}, {"/search/valid", false},
{"/user_:name", false}, {"/user_:name", false},
{"/user_x", true}, {"/user_x", false},
{"/user_:name", false}, {"/user_:name", false},
{"/id:id", false}, {"/id:id", false},
{"/id/:id", true}, {"/id/:id", false},
}
testRoutes(t, routes)
}
func TestCatchAllAfterSlash(t *testing.T) {
routes := []testRoute{
{"/non-leading-*catchall", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -283,14 +421,17 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) { func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{ routes := []testRoute{
{"/cmd/vet", false}, {"/cmd/vet", false},
{"/cmd/:tool/:sub", true}, {"/cmd/:tool", false},
{"/cmd/:tool/:sub", false},
{"/cmd/:tool/misc", false},
{"/cmd/:tool/:othersub", true},
{"/src/AUTHORS", false}, {"/src/AUTHORS", false},
{"/src/*filepath", true}, {"/src/*filepath", true},
{"/user_x", false}, {"/user_x", false},
{"/user_:name", true}, {"/user_:name", false},
{"/id/:id", false}, {"/id/:id", false},
{"/id:id", true}, {"/id:id", false},
{"/:id", true}, {"/:id", false},
{"/*filepath", true}, {"/*filepath", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
@ -323,12 +464,14 @@ func TestTreeDupliatePath(t *testing.T) {
} }
} }
//printChildren(tree, "")
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/doc/", false, "/doc/", nil}, {"/doc/", false, "/doc/", nil},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
}) })
} }
@ -356,6 +499,8 @@ func TestTreeCatchAllConflict(t *testing.T) {
{"/src/*filepath/x", true}, {"/src/*filepath/x", true},
{"/src2/", false}, {"/src2/", false},
{"/src2/*filepath/x", true}, {"/src2/*filepath/x", true},
{"/src3/*filepath", false},
{"/src3/*filepath/x", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -368,6 +513,12 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
testRoutes(t, routes) testRoutes(t, routes)
} }
func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{}
var route = "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route))
}
func TestTreeDoubleWildcard(t *testing.T) { func TestTreeDoubleWildcard(t *testing.T) {
const panicMsg = "only one wildcard per path segment is allowed" const panicMsg = "only one wildcard per path segment is allowed"
@ -454,10 +605,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/", "/doc/",
} }
for _, route := range tsrRoutes { for _, route := range tsrRoutes {
handler, _, tsr := tree.getValue(route, nil, false) value := tree.getValue(route, nil, false)
if handler != nil { if value.handlers != nil {
t.Fatalf("non-nil handler for TSR route '%s", route) t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !tsr { } else if !value.tsr {
t.Errorf("expected TSR recommendation for route '%s'", route) t.Errorf("expected TSR recommendation for route '%s'", route)
} }
} }
@ -471,10 +622,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/api/world/abc", "/api/world/abc",
} }
for _, route := range noTsrRoutes { for _, route := range noTsrRoutes {
handler, _, tsr := tree.getValue(route, nil, false) value := tree.getValue(route, nil, false)
if handler != nil { if value.handlers != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route) t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if tsr { } else if value.tsr {
t.Errorf("expected no TSR recommendation for route '%s'", route) t.Errorf("expected no TSR recommendation for route '%s'", route)
} }
} }
@ -490,10 +641,10 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv) t.Fatalf("panic inserting test route: %v", recv)
} }
handler, _, tsr := tree.getValue("/", nil, false) value := tree.getValue("/", nil, false)
if handler != nil { if value.handlers != nil {
t.Fatalf("non-nil handler") t.Fatalf("non-nil handler")
} else if tsr { } else if value.tsr {
t.Errorf("expected no TSR recommendation") t.Errorf("expected no TSR recommendation")
} }
} }
@ -501,6 +652,9 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
func TestTreeFindCaseInsensitivePath(t *testing.T) { func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{} tree := &node{}
longPath := "/l" + strings.Repeat("o", 128) + "ng"
lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
routes := [...]string{ routes := [...]string{
"/hi", "/hi",
"/b/", "/b/",
@ -524,6 +678,17 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
"/doc/go/away", "/doc/go/away",
"/no/a", "/no/a",
"/no/b", "/no/b",
"/Π",
"/u/apfêl/",
"/u/äpfêl/",
"/u/öpfêl",
"/v/Äpfêl/",
"/v/Öpfêl",
"/w/♬", // 3 byte
"/w/♭/", // 3 byte, last byte differs
"/w/𠜎", // 4 byte
"/w/𠜏/", // 4 byte
longPath,
} }
for _, route := range routes { for _, route := range routes {
@ -602,6 +767,21 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
{"/DOC/", "/doc", true, true}, {"/DOC/", "/doc", true, true},
{"/NO", "", false, true}, {"/NO", "", false, true},
{"/DOC/GO", "", false, true}, {"/DOC/GO", "", false, true},
{"/π", "/Π", true, false},
{"/π/", "/Π", true, true},
{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
{"/u/ÖPFÊL", "/u/öpfêl", true, false},
{"/v/äpfêL/", "/v/Äpfêl/", true, false},
{"/v/äpfêL", "/v/Äpfêl/", true, true},
{"/v/öpfêL/", "/v/Öpfêl", true, true},
{"/v/öpfêL", "/v/Öpfêl", true, false},
{"/w/♬/", "/w/♬", true, true},
{"/w/♭", "/w/♭/", true, true},
{"/w/𠜎/", "/w/𠜎", true, true},
{"/w/𠜏", "/w/𠜏/", true, true},
{lOngPath, longPath, true, true},
} }
// With fixTrailingSlash = true // With fixTrailingSlash = true
for _, test := range tests { for _, test := range tests {
@ -656,6 +836,19 @@ func TestTreeInvalidNodeType(t *testing.T) {
} }
} }
func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{}
tree.wildChild = true
tree.children = append(tree.children, &node{})
tree.children[0].nType = 2
// set invalid Params type
params := make(Params, 0, 0)
// try to trigger slice bounds out of range with capacity 0
tree.getValue("/test", &params, false)
}
func TestTreeWildcardConflictEx(t *testing.T) { func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct { conflicts := [...]struct {
route string route string
@ -666,8 +859,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`}, {"/con:nection", ":nection", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
} }
for _, conflict := range conflicts { for _, conflict := range conflicts {
@ -689,8 +881,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
tree.addRoute(conflict.route, fakeHandler(conflict.route)) tree.addRoute(conflict.route, fakeHandler(conflict.route))
}) })
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv) t.Fatalf("invalid wildcard conflict error (%v)", recv)
} }
} }

View File

@ -90,20 +90,23 @@ func filterFlags(content string) string {
} }
func chooseData(custom, wildcard interface{}) interface{} { func chooseData(custom, wildcard interface{}) interface{} {
if custom == nil { if custom != nil {
if wildcard == nil { return custom
panic("negotiation config is invalid") }
} if wildcard != nil {
return wildcard return wildcard
} }
return custom panic("negotiation config is invalid")
} }
func parseAccept(acceptHeader string) []string { func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",") parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts)) out := make([]string, 0, len(parts))
for _, part := range parts { for _, part := range parts {
if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { if i := strings.IndexByte(part, ';'); i > 0 {
part = part[:i]
}
if part = strings.TrimSpace(part); part != "" {
out = append(out, part) out = append(out, part)
} }
} }
@ -127,8 +130,7 @@ func joinPaths(absolutePath, relativePath string) string {
} }
finalPath := path.Join(absolutePath, relativePath) finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
if appendSlash {
return finalPath + "/" return finalPath + "/"
} }
return finalPath return finalPath
@ -146,6 +148,6 @@ func resolveAddress(addr []string) string {
case 1: case 1:
return addr[0] return addr[0]
default: default:
panic("too much parameters") panic("too many parameters")
} }
} }

View File

@ -18,6 +18,12 @@ func init() {
SetMode(TestMode) SetMode(TestMode)
} }
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}
type testStruct struct { type testStruct struct {
T *testing.T T *testing.T
} }

121
vendor/vendor.json vendored
View File

@ -1,121 +0,0 @@
{
"comment": "v1.4.0",
"ignore": "test",
"package": [
{
"checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=",
"path": "github.com/davecgh/go-spew/spew",
"revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
"revisionTime": "2018-02-21T22:46:20Z",
"version": "v1.1",
"versionExact": "v1.1.1"
},
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
"path": "github.com/gin-contrib/sse",
"revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae",
"revisionTime": "2019-03-01T06:25:29Z"
},
{
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
"path": "github.com/golang/protobuf/proto",
"revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
"revisionTime": "2019-02-05T22:20:52Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
"path": "github.com/json-iterator/go",
"revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
"revisionTime": "2019-03-06T14:29:09Z",
"version": "v1.1",
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "Ya+baVBU/RkXXUWD3LGFmGJiiIg=",
"path": "github.com/mattn/go-isatty",
"revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
"revisionTime": "2019-03-12T13:58:54Z",
"version": "v0.0",
"versionExact": "v0.0.7"
},
{
"checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=",
"path": "github.com/modern-go/concurrent",
"revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
"revisionTime": "2018-03-06T01:26:44Z"
},
{
"checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=",
"path": "github.com/modern-go/reflect2",
"revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8",
"revisionTime": "2018-07-18T01:23:57Z"
},
{
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
"path": "github.com/pmezard/go-difflib/difflib",
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
"revisionTime": "2018-12-26T10:54:42Z"
},
{
"checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=",
"path": "github.com/stretchr/objx",
"revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c",
"revisionTime": "2019-02-11T16:23:28Z"
},
{
"checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=",
"path": "github.com/stretchr/testify",
"revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
"revisionTime": "2018-12-05T02:12:43Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert",
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
"revisionTime": "2018-05-06T18:05:49Z",
"version": "v1.2",
"versionExact": "v1.2.2"
},
{
"checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=",
"path": "github.com/ugorji/go/codec",
"revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13",
"revisionTime": "2019-04-08T19:08:48Z",
"version": "v1.1",
"versionExact": "v1.1.4"
},
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
"revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7",
"revisionTime": "2019-05-02T22:26:14Z"
},
{
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"path": "golang.org/x/sys/unix",
"revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
"revisionTime": "2019-05-02T15:41:39Z"
},
{
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
"path": "gopkg.in/go-playground/validator.v8",
"revision": "5f1438d3fca68893a817e4a66806cea46a9e4ebf",
"revisionTime": "2017-07-30T05:02:35Z",
"version": "v8.18.2",
"versionExact": "v8.18.2"
},
{
"checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
"path": "gopkg.in/yaml.v2",
"revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
"revisionTime": "2018-11-15T11:05:04Z",
"version": "v2.2",
"versionExact": "v2.2.2"
}
],
"rootPath": "github.com/gin-gonic/gin"
}

View File

@ -5,4 +5,4 @@
package gin package gin
// Version is the current gin framework's version. // Version is the current gin framework's version.
const Version = "v1.4.0" const Version = "v1.7.3"