Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	go.mod
#	go.sum
This commit is contained in:
fifsky 2021-04-08 22:18:13 +08:00
commit 806032f73a
62 changed files with 3580 additions and 1971 deletions

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 TravisCI.
- 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

@ -3,11 +3,20 @@ language: go
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x - go: 1.12.x
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
- go: 1.13.x
env:
- TESTTAGS=nomsgpack
- go: 1.14.x
- go: 1.14.x
env:
- TESTTAGS=nomsgpack
- go: 1.15.x
- go: 1.15.x
env:
- TESTTAGS=nomsgpack
- go: master - go: master
git: git:
@ -17,7 +26,7 @@ before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install: install:
- if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi - if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi - if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi

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,4 +1,107 @@
### Gin v1.5.0 # Gin ChangeLog
## 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) - [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) - [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
@ -90,7 +193,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 v1.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)
@ -112,7 +215,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
@ -135,25 +238,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
@ -188,7 +291,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
@ -196,7 +299,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
@ -232,7 +335,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
@ -242,14 +345,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
@ -258,7 +361,7 @@
- [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()
@ -276,7 +379,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
@ -295,7 +398,7 @@
- [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 TravisCI.
- 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,16 @@
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/) VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") 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 +44,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 \

357
README.md
View File

@ -5,36 +5,41 @@
[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
## Contents ## Contents
- [Installation](#installation) - [Gin Web Framework](#gin-web-framework)
- [Prerequisite](#prerequisite) - [Contents](#contents)
- [Quick start](#quick-start) - [Installation](#installation)
- [Benchmarks](#benchmarks) - [Quick start](#quick-start)
- [Gin v1.stable](#gin-v1-stable) - [Benchmarks](#benchmarks)
- [Build with jsoniter](#build-with-jsoniter) - [Gin v1. stable](#gin-v1-stable)
- [API Examples](#api-examples) - [Build with jsoniter](#build-with-jsoniter)
- [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) - [API Examples](#api-examples)
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path) - [Parameters in path](#parameters-in-path)
- [Querystring parameters](#querystring-parameters) - [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form) - [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form) - [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files) - [Upload files](#upload-files)
- [Single file](#single-file)
- [Multiple files](#multiple-files)
- [Grouping routes](#grouping-routes) - [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware) - [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file) - [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format) - [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Model binding and validation](#model-binding-and-validation) - [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
@ -44,10 +49,17 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
- [JSONP rendering](#jsonp) - [SecureJSON](#securejson)
- [JSONP](#jsonp)
- [AsciiJSON](#asciijson)
- [PureJSON](#purejson)
- [Serving static files](#serving-static-files) - [Serving static files](#serving-static-files)
- [Serving data from file](#serving-data-from-file)
- [Serving data from reader](#serving-data-from-reader) - [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering) - [HTML rendering](#html-rendering)
- [Custom Template renderer](#custom-template-renderer)
- [Custom Delimiters](#custom-delimiters)
- [Custom Template Funcs](#custom-template-funcs)
- [Multitemplate](#multitemplate) - [Multitemplate](#multitemplate)
- [Redirects](#redirects) - [Redirects](#redirects)
- [Custom Middleware](#custom-middleware) - [Custom Middleware](#custom-middleware)
@ -56,21 +68,23 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Custom HTTP configuration](#custom-http-configuration) - [Custom HTTP configuration](#custom-http-configuration)
- [Support Let's Encrypt](#support-lets-encrypt) - [Support Let's Encrypt](#support-lets-encrypt)
- [Run multiple service using Gin](#run-multiple-service-using-gin) - [Run multiple service using Gin](#run-multiple-service-using-gin)
- [Graceful restart or stop](#graceful-restart-or-stop) - [Graceful shutdown or restart](#graceful-shutdown-or-restart)
- [Third-party packages](#third-party-packages)
- [Manually](#manually)
- [Build a single binary with templates](#build-a-single-binary-with-templates) - [Build a single binary with templates](#build-a-single-binary-with-templates)
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
- [Try to bind body into different structs](#try-to-bind-body-into-different-structs) - [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
- [http2 server push](#http2-server-push) - [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie) - [Set and get a cookie](#set-and-get-a-cookie)
- [Testing](#testing) - [Testing](#testing)
- [Users](#users) - [Users](#users)
## Installation ## Installation
To install Gin package, you need to install Go and set your Go workspace first. To install Gin package, you need to install Go and set your Go workspace first.
1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin. 1. The first need [Go](https://golang.org/) installed (**version 1.12+ is required**), then you can use the below Go command to install Gin.
```sh ```sh
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
@ -88,46 +102,8 @@ import "github.com/gin-gonic/gin"
import "net/http" import "net/http"
``` ```
### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
1. `go get` govendor
```sh
$ go get github.com/kardianos/govendor
```
2. Create your project folder and `cd` inside
```sh
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
If you are on a Mac and you're installing Go 1.8 (released: Feb 2017) or later, GOPATH is automatically determined by the Go toolchain for you. It defaults to $HOME/go on macOS so you can create your project like this
```sh
$ mkdir -p $HOME/go/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.3
```
4. Copy a starting template inside your project
```sh
$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
```
5. Run your project
```sh
$ go run main.go
```
## Quick start ## Quick start
```sh ```sh
# assume the following codes in example.go file # assume the following codes in example.go file
$ cat example.go $ cat example.go
@ -160,35 +136,38 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
[See all benchmarks](/BENCHMARKS.md) [See all benchmarks](/BENCHMARKS.md)
Benchmark name | (1) | (2) | (3) | (4) | Benchmark name | (1) | (2) | (3) | (4) |
--------------------------------------------|-----------:|------------:|-----------:|---------: | ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
**BenchmarkGin_GithubAll** | **30000** | **48375** | **0** | **0** | BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
BenchmarkAce_GithubAll | 10000 | 134059 | 13792 | 167 | BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
BenchmarkBear_GithubAll | 5000 | 534445 | 86448 | 943 | BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
BenchmarkBeego_GithubAll | 3000 | 592444 | 74705 | 812 | BenchmarkBear_GithubAll | 9234 | 216179 ns/op | 86448 B/op | 943 allocs/op |
BenchmarkBone_GithubAll | 200 | 6957308 | 698784 | 8453 | BenchmarkBeego_GithubAll | 7407 | 243496 ns/op | 71456 B/op | 609 allocs/op |
BenchmarkDenco_GithubAll | 10000 | 158819 | 20224 | 167 | BenchmarkBone_GithubAll | 420 | 2922835 ns/op | 720160 B/op | 8620 allocs/op |
BenchmarkEcho_GithubAll | 10000 | 154700 | 6496 | 203 | BenchmarkChi_GithubAll | 7620 | 238331 ns/op | 87696 B/op | 609 allocs/op |
BenchmarkGocraftWeb_GithubAll | 3000 | 570806 | 131656 | 1686 | BenchmarkDenco_GithubAll | 18355 | 64494 ns/op | 20224 B/op | 167 allocs/op |
BenchmarkGoji_GithubAll | 2000 | 818034 | 56112 | 334 | BenchmarkEcho_GithubAll | 31251 | 38479 ns/op | 0 B/op | 0 allocs/op |
BenchmarkGojiv2_GithubAll | 2000 | 1213973 | 274768 | 3712 | BenchmarkGocraftWeb_GithubAll | 4117 | 300062 ns/op | 131656 B/op | 1686 allocs/op |
BenchmarkGoJsonRest_GithubAll | 2000 | 785796 | 134371 | 2737 | BenchmarkGoji_GithubAll | 3274 | 416158 ns/op | 56112 B/op | 334 allocs/op |
BenchmarkGoRestful_GithubAll | 300 | 5238188 | 689672 | 4519 | BenchmarkGojiv2_GithubAll | 1402 | 870518 ns/op | 352720 B/op | 4321 allocs/op |
BenchmarkGorillaMux_GithubAll | 100 | 10257726 | 211840 | 2272 | BenchmarkGoJsonRest_GithubAll | 2976 | 401507 ns/op | 134371 B/op | 2737 allocs/op |
BenchmarkHttpRouter_GithubAll | 20000 | 105414 | 13792 | 167 | BenchmarkGoRestful_GithubAll | 410 | 2913158 ns/op | 910144 B/op | 2938 allocs/op |
BenchmarkHttpTreeMux_GithubAll | 10000 | 319934 | 65856 | 671 | BenchmarkGorillaMux_GithubAll | 346 | 3384987 ns/op | 251650 B/op | 1994 allocs/op |
BenchmarkKocha_GithubAll | 10000 | 209442 | 23304 | 843 | BenchmarkGowwwRouter_GithubAll | 10000 | 143025 ns/op | 72144 B/op | 501 allocs/op |
BenchmarkLARS_GithubAll | 20000 | 62565 | 0 | 0 | BenchmarkHttpRouter_GithubAll | 55938 | 21360 ns/op | 0 B/op | 0 allocs/op |
BenchmarkMacaron_GithubAll | 2000 | 1161270 | 204194 | 2000 | BenchmarkHttpTreeMux_GithubAll | 10000 | 153944 ns/op | 65856 B/op | 671 allocs/op |
BenchmarkMartini_GithubAll | 200 | 9991713 | 226549 | 2325 | BenchmarkKocha_GithubAll | 10000 | 106315 ns/op | 23304 B/op | 843 allocs/op |
BenchmarkPat_GithubAll | 200 | 5590793 | 1499568 | 27435 | BenchmarkLARS_GithubAll | 47779 | 25084 ns/op | 0 B/op | 0 allocs/op |
BenchmarkPossum_GithubAll | 10000 | 319768 | 84448 | 609 | BenchmarkMacaron_GithubAll | 3266 | 371907 ns/op | 149409 B/op | 1624 allocs/op |
BenchmarkR2router_GithubAll | 10000 | 305134 | 77328 | 979 | BenchmarkMartini_GithubAll | 331 | 3444706 ns/op | 226551 B/op | 2325 allocs/op |
BenchmarkRivet_GithubAll | 10000 | 132134 | 16272 | 167 | BenchmarkPat_GithubAll | 273 | 4381818 ns/op | 1483152 B/op | 26963 allocs/op |
BenchmarkTango_GithubAll | 3000 | 552754 | 63826 | 1618 | BenchmarkPossum_GithubAll | 10000 | 164367 ns/op | 84448 B/op | 609 allocs/op |
BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104 | 5374 | BenchmarkR2router_GithubAll | 10000 | 160220 ns/op | 77328 B/op | 979 allocs/op |
BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 | BenchmarkRivet_GithubAll | 14625 | 82453 ns/op | 16272 B/op | 167 allocs/op |
BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 | BenchmarkTango_GithubAll | 6255 | 279611 ns/op | 63826 B/op | 1618 allocs/op |
| BenchmarkTigerTonic_GithubAll | 2008 | 687874 ns/op | 193856 B/op | 4474 allocs/op |
| BenchmarkTraffic_GithubAll | 355 | 3478508 ns/op | 820744 B/op | 14114 allocs/op |
| BenchmarkVulcan_GithubAll | 6885 | 193333 ns/op | 19894 B/op | 609 allocs/op |
- (1): Total Repetitions achieved in constant time, higher means more confident result - (1): Total Repetitions achieved in constant time, higher means more confident result
- (2): Single Repetition Duration (ns/op), lower is better - (2): Single Repetition Duration (ns/op), lower is better
@ -199,8 +178,8 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [x] Zero allocation router. - [x] Zero allocation router.
- [x] Still the fastest http router and framework. From routing to writing. - [x] Still the fastest http router and framework. From routing to writing.
- [x] Complete suite of unit tests - [x] Complete suite of unit tests.
- [x] Battle tested - [x] Battle tested.
- [x] API frozen, new releases will not break your code. - [x] API frozen, new releases will not break your code.
## Build with [jsoniter](https://github.com/json-iterator/go) ## Build with [jsoniter](https://github.com/json-iterator/go)
@ -264,6 +243,13 @@ func main() {
c.FullPath() == "/user/:name/*action" // true c.FullPath() == "/user/:name/*action" // true
}) })
// This handler will add a new router for /user/groups.
// Exact routes are resolved before param routes, regardless of the order they were defined.
// Routes starting with /user/groups are never interpreted as /user/:name/... routes
router.GET("/user/groups", func(c *gin.Context) {
c.String(http.StatusOK, "The available groups are [...]", name)
})
router.Run(":8080") router.Run(":8080")
} }
``` ```
@ -361,7 +347,7 @@ func main() {
``` ```
``` ```
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
``` ```
### Upload files ### Upload files
@ -378,14 +364,14 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB) // Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file // single file
file, _ := c.FormFile("file") file, _ := c.FormFile("file")
log.Println(file.Filename) log.Println(file.Filename)
// Upload the file to specific dst. // Upload the file to specific dst.
// c.SaveUploadedFile(file, dst) c.SaveUploadedFile(file, dst)
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
}) })
@ -409,7 +395,7 @@ See the detail [example code](https://github.com/gin-gonic/examples/tree/master/
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB) // Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// Multipart form // Multipart form
form, _ := c.MultipartForm() form, _ := c.MultipartForm()
@ -419,7 +405,7 @@ func main() {
log.Println(file.Filename) log.Println(file.Filename)
// Upload the file to specific dst. // Upload the file to specific dst.
// c.SaveUploadedFile(file, dst) c.SaveUploadedFile(file, dst)
} }
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
}) })
@ -517,6 +503,39 @@ func main() {
} }
``` ```
### Custom Recovery behavior
```go
func main() {
// Creates a router without any middleware by default
r := gin.New()
// Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger())
// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
if err, ok := recovered.(string); ok {
c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
}
c.AbortWithStatus(http.StatusInternalServerError)
}))
r.GET("/panic", func(c *gin.Context) {
// panic with a string -- the custom middleware could save this to a database or report it to the user
panic("foo")
})
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "ohai")
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
### How to write log file ### How to write log file
```go ```go
func main() { func main() {
@ -576,44 +595,44 @@ func main() {
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
``` ```
### Controlling Log output coloring ### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY. By default, logs output on console should be colorized depending on the detected TTY.
Never colorize logs: Never colorize logs:
```go ```go
func main() { func main() {
// Disable log's color // Disable log's color
gin.DisableConsoleColor() gin.DisableConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
router.GET("/ping", func(c *gin.Context) { router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(200, "pong")
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
Always colorize logs: Always colorize logs:
```go ```go
func main() { func main() {
// Force log's color // Force log's color
gin.ForceConsoleColor() gin.ForceConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
router.GET("/ping", func(c *gin.Context) { router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(200, "pong")
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
@ -622,7 +641,7 @@ func main() {
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#hdr-Baked_In_Validators_and_Tags).
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
@ -655,12 +674,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if json.User != "manu" || json.Password != "123" { if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -676,12 +695,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if xml.User != "manu" || xml.Password != "123" { if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -693,12 +712,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if form.User != "manu" || form.Password != "123" { if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -742,12 +761,11 @@ package main
import ( import (
"net/http" "net/http"
"reflect"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8" "github.com/go-playground/validator/v10"
) )
// Booking contains binded and validated data. // Booking contains binded and validated data.
@ -756,11 +774,9 @@ type Booking struct {
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
} }
func bookableDate( var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, date, ok := fl.Field().Interface().(time.Time)
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, if ok {
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now() today := time.Now()
if today.After(date) { if today.After(date) {
return false return false
@ -791,11 +807,14 @@ func getBookable(c *gin.Context) {
``` ```
```console ```console
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" $ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-04-17"
{"message":"Booking dates are valid!"} {"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" $ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} {"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
``` ```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. [Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
@ -1133,7 +1152,7 @@ func main() {
data := gin.H{ data := gin.H{
"foo": "bar", "foo": "bar",
} }
//callback is x //callback is x
// Will output : x({\"foo\":\"bar\"}) // Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data) c.JSONP(http.StatusOK, data)
@ -1178,21 +1197,21 @@ This feature is unavailable in Go 1.6 and lower.
```go ```go
func main() { func main() {
r := gin.Default() r := gin.Default()
// Serves unicode entities // Serves unicode entities
r.GET("/json", func(c *gin.Context) { r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>", "html": "<b>Hello, world!</b>",
}) })
}) })
// Serves literal characters // Serves literal characters
r.GET("/purejson", func(c *gin.Context) { r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{ c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>", "html": "<b>Hello, world!</b>",
}) })
}) })
// listen and serve on 0.0.0.0:8080 // listen and serve on 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
} }
@ -1212,6 +1231,24 @@ func main() {
} }
``` ```
### Serving data from file
```go
func main() {
router := gin.Default()
router.GET("/local/file", func(c *gin.Context) {
c.File("local/file.go")
})
var fs http.FileSystem = // ...
router.GET("/fs/file", func(c *gin.Context) {
c.FileFromFS("fs/file.go", fs)
})
}
```
### Serving data from reader ### Serving data from reader
```go ```go
@ -1225,6 +1262,7 @@ func main() {
} }
reader := response.Body reader := response.Body
defer reader.Close()
contentLength := response.ContentLength contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type") contentType := response.Header.Get("Content-Type")
@ -1400,6 +1438,12 @@ r.GET("/test", func(c *gin.Context) {
}) })
``` ```
Issuing a HTTP redirect from POST. Refer to issue: [#444](https://github.com/gin-gonic/gin/issues/444)
```go
r.POST("/test", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/foo")
})
```
Issuing a Router redirect, use `HandleContext` like below. Issuing a Router redirect, use `HandleContext` like below.
@ -1699,12 +1743,13 @@ func main() {
} }
``` ```
### Graceful restart or stop ### Graceful shutdown or restart
Do you want to graceful restart or stop your web server? There are a few approaches you can use to perform a graceful shutdown or restart. You can make use of third-party packages specifically built for that, or you can manually do the same with the functions and methods from the built-in packages.
There are some ways this can be done.
We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. #### Third-party packages
We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer to issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details.
```go ```go
router := gin.Default() router := gin.Default()
@ -1713,13 +1758,15 @@ router.GET("/", handler)
endless.ListenAndServe(":4242", router) endless.ListenAndServe(":4242", router)
``` ```
An alternative to endless: Alternatives:
* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. * [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. * [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. * [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. #### Manually
In case you are using Go 1.8 or a later version, you may not need to use those libraries. Consider using `http.Server`'s built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. The example below describes its usage, and we've got more examples using gin [here](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown).
```go ```go
// +build go1.8 // +build go1.8
@ -1750,10 +1797,11 @@ func main() {
Handler: router, Handler: router,
} }
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() { go func() {
// service connections if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("listen: %s\n", err)
log.Fatalf("listen: %s\n", err)
} }
}() }()
@ -1765,18 +1813,17 @@ func main() {
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Println("Shutdown Server ...") log.Println("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err) log.Fatal("Server forced to shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
} }
log.Println("Server exiting") log.Println("Server exiting")
} }
``` ```
@ -2077,6 +2124,39 @@ func main() {
} }
``` ```
## Don't trust all proxies
Gin lets you specify which headers to hold the real client IP (if any),
as well as specifying which proxies (or direct clients) you trust to
specify one of these headers.
The `TrustedProxies` slice on your `gin.Engine` specifes network addresses or
network CIDRs from where clients which their request headers related to client
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs.
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.TrustedProxies = []string{"192.168.1.2"}
router.GET("/", func(c *gin.Context) {
// If the client is 192.168.1.2, use the X-Forwarded-For
// header to deduce the original client IP from the trust-
// worthy parts of that header.
// Otherwise, simply return the direct client IP
fmt.Printf("ClientIP: %s\n", c.ClientIP())
})
router.Run()
}
```
## Testing ## Testing
@ -2134,3 +2214,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.

12
auth.go
View File

@ -5,9 +5,12 @@
package gin package gin
import ( import (
"crypto/subtle"
"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.
@ -28,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
} }
} }
@ -68,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)
@ -83,5 +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))
} }

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"
@ -49,7 +52,8 @@ type BindingUri interface {
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v8.18.2.
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.
@ -84,7 +88,7 @@ var (
// 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

@ -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/v8.18.2.
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/v8.18.2
// 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,7 +22,6 @@ 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 { type appkey struct {
@ -35,7 +35,7 @@ type QueryTest struct {
} }
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 {
@ -163,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))
} }
@ -184,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",
@ -204,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`) `{"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",
"/", "/", "/", "/",
@ -340,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", "/",
@ -370,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",
@ -391,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",
@ -633,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"}`)
@ -1138,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())
@ -1250,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,7 +5,9 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@ -16,22 +18,54 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type sliceValidateError []error
func (err sliceValidateError) Error() string {
var errMsgs []string
for i, e := range err {
if e == nil {
continue
}
errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
}
return strings.Join(errMsgs, "\n")
}
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

View File

@ -0,0 +1,68 @@
// 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"},
}
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

@ -8,7 +8,7 @@ import (
"net/http" "net/http"
) )
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 << 20
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{} type formPostBinding struct{}

View File

@ -12,6 +12,7 @@ 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"
) )
@ -28,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error {
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)
} }
@ -208,9 +224,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
} }
@ -269,7 +285,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano": case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 0) tv, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }
@ -348,3 +364,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 errors.New("cannot convert to map slices of strings")
}
for k, v := range form {
ptrMap[k] = v
}
return nil
}
ptrMap, ok := ptr.(map[string]string)
if !ok {
return errors.New("cannot convert to map of strings")
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
return nil
}

View File

@ -190,7 +190,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
} }

View File

@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo) 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 (

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 (

View File

@ -16,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/gin-contrib/sse" "github.com/gin-contrib/sse"
@ -33,9 +34,11 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
// BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
const abortIndex int8 = math.MaxInt8 / 2 const abortIndex int8 = math.MaxInt8 / 2
// 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,
@ -51,6 +54,10 @@ type Context struct {
fullPath string 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{}
@ -67,6 +74,10 @@ type Context struct {
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH, // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters. // or PUT body parameters.
formCache url.Values 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
} }
/************************************/ /************************************/
@ -78,18 +89,25 @@ func (c *Context) reset() {
c.Params = c.Params[0:0] c.Params = c.Params[0:0]
c.handlers = nil c.handlers = nil
c.index = -1 c.index = -1
c.fullPath = "" c.fullPath = ""
c.Keys = nil c.Keys = nil
c.Errors = c.Errors[0:0] c.Errors = c.Errors[0:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
c.formCache = nil c.formCache = nil
*c.params = (*c.params)[0: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
@ -219,16 +237,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
} }
@ -272,6 +295,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 {
@ -391,16 +430,20 @@ func (c *Context) QueryArray(key string) []string {
return values return values
} }
func (c *Context) getQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { if c.queryCache == nil {
c.queryCache = c.Request.URL.Query() 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) {
c.getQueryCache() c.initQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 { if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true return values, true
} }
@ -416,7 +459,7 @@ 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) {
c.getQueryCache() c.initQueryCache()
return c.get(c.queryCache, key) return c.get(c.queryCache, key)
} }
@ -458,7 +501,7 @@ func (c *Context) PostFormArray(key string) []string {
return values return values
} }
func (c *Context) getFormCache() { func (c *Context) initFormCache() {
if c.formCache == nil { if c.formCache == nil {
c.formCache = make(url.Values) c.formCache = make(url.Values)
req := c.Request req := c.Request
@ -474,7 +517,7 @@ func (c *Context) getFormCache() {
// 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) {
c.getFormCache() c.initFormCache()
if values := c.formCache[key]; len(values) > 0 { if values := c.formCache[key]; len(values) > 0 {
return values, true return values, true
} }
@ -490,7 +533,7 @@ 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) {
c.getFormCache() c.initFormCache()
return c.get(c.formCache, key) return c.get(c.formCache, key)
} }
@ -682,32 +725,82 @@ 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 {
clientIP := c.requestHeader("X-Forwarded-For")
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
}
if clientIP != "" {
return clientIP
}
}
if c.engine.AppEngine { if c.engine.AppEngine {
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
}
trustedCIDRs, _ := c.engine.prepareTrustedCIDRs()
c.engine.trustedCIDRs = trustedCIDRs
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.
@ -772,6 +865,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.
@ -785,6 +883,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,
}) })
@ -838,11 +937,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", "")
@ -919,15 +1018,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)
} }
@ -969,6 +1079,7 @@ type Negotiate struct {
HTMLData interface{} HTMLData interface{}
JSONData interface{} JSONData interface{}
XMLData interface{} XMLData interface{}
YAMLData interface{}
Data interface{} Data interface{}
} }
@ -987,6 +1098,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
} }
@ -1003,20 +1118,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
} }
} }
} }
@ -1032,26 +1147,20 @@ 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 always returns that there is no deadline (ok==false),
// should be canceled. Deadline returns ok==false when no deadline is // maybe you want to use Request.Context().Deadline() instead.
// 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 return
} }
// Done returns a channel that's closed when work done on behalf of this // Done always returns nil (chan which will wait forever),
// context should be canceled. Done may return nil if this context can // if you want to abort your work when the connection was closed
// never be canceled. Successive calls to Done return the same value. // you should use Request.Context().Done() instead.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
return nil return nil
} }
// Err returns a non-nil error value after Done is closed, // Err always returns nil, maybe you want to use Request.Context().Err() instead.
// 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 return nil
} }

View File

@ -1,9 +1,10 @@
// +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() {

View File

@ -261,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)
@ -410,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")
@ -602,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) {
@ -662,7 +691,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"}) c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -690,7 +719,7 @@ func TestContextRenderJSONPWithoutCallback(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, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -716,7 +745,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar"}) c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type")) assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
} }
@ -938,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()
@ -992,6 +1021,19 @@ func TestContextRenderFile(t *testing.T) {
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"))
} }
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 {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
}
func TestContextRenderAttachment(t *testing.T) { func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1114,12 +1156,12 @@ 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"},
}) })
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -1129,7 +1171,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"},
}) })
@ -1240,7 +1282,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()
@ -1283,7 +1325,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\"}\n"), jsonStringBody) assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
} }
func TestContextError(t *testing.T) { func TestContextError(t *testing.T) {
@ -1350,11 +1392,10 @@ 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.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") resetContextForClientIPTests(c)
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.RemoteAddr = " 40.40.40.40:42123 "
// Legacy tests (validating that the defaults don't break the
// (insecure!) old behaviour)
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")
@ -1374,6 +1415,74 @@ 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.TrustedProxies = []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.TrustedProxies = []string{"30.30.30.30"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
c.engine.TrustedProxies = []string{"40.40.40.40"}
assert.Equal(t, "20.20.20.20", c.ClientIP())
// All steps are trusted
c.engine.TrustedProxies = []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.TrustedProxies = []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.TrustedProxies = []string{"foo"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
c.engine.TrustedProxies = []string{"bar"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
c.engine.TrustedProxies = []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.TrustedProxies = []string{"baz"}
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedProxies = []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.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// 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.RemoteAddr = " 40.40.40.40:42123 "
c.engine.AppEngine = false
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -1905,3 +2014,25 @@ func TestRaceParamsContextCopy(t *testing.T) {
performRequest(router, "GET", "/name2/api") performRequest(router, "GET", "/name2/api")
wg.Wait() 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)
}

View File

@ -12,7 +12,7 @@ import (
"strings" "strings"
) )
const ginSupportMinGoVer = 10 const ginSupportMinGoVer = 12
// 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.
@ -67,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.11 or later and Go 1.12 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.12+.
`) `)
} }

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.11 or later and Go 1.12 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.12+.\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)
} }

View File

@ -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
} }
} }

34
errors_1.13_test.go Normal file
View File

@ -0,0 +1,34 @@
//go:build go1.13
// +build go1.13
package gin
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
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,
// hence the "// +build go1.13" directive at the beginning of this file.
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))
}

6
fs.go
View File

@ -9,7 +9,7 @@ import (
"os" "os"
) )
type onlyfilesFS struct { type onlyFilesFS struct {
fs http.FileSystem fs http.FileSystem
} }
@ -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

155
gin.go
View File

@ -11,19 +11,22 @@ 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 defaultAppEngine bool
// 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)
@ -79,9 +82,26 @@ 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
// 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
// #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
@ -97,8 +117,12 @@ type Engine struct {
// method call. // method call.
MaxMultipartMemory int64 MaxMultipartMemory int64
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
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 +131,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 +158,16 @@ func New() *Engine {
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine, AppEngine: defaultAppEngine,
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 +185,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 +195,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,6 +279,7 @@ 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)
@ -256,6 +287,11 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
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:
@ -290,12 +326,60 @@ 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) }()
trustedCIDRs, err := engine.prepareTrustedCIDRs()
if err != nil {
return err
}
engine.trustedCIDRs = trustedCIDRs
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
} }
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
}
// 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)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -314,16 +398,13 @@ 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)
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()
err = os.Chmod(file, 0777) defer os.Remove(file)
if err != nil {
return
}
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -385,7 +466,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
@ -395,10 +479,12 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
value := root.getValue(rPath, c.Params, unescape) value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil { if value.handlers != nil {
c.handlers = value.handlers c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath c.fullPath = value.fullPath
c.Next() c.Next()
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
@ -457,18 +543,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 {
@ -476,15 +555,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"
@ -54,6 +55,13 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
}
func TestRunTLS(t *testing.T) { func TestRunTLS(t *testing.T) {
router := New() router := New()
go func() { go func() {
@ -146,15 +154,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")
@ -291,7 +303,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()

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.

12
go.mod
View File

@ -1,14 +1,14 @@
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.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.0.1 github.com/go-playground/validator/v10 v10.4.1
github.com/golang/protobuf v1.3.2 github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.7 github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.9 github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7 github.com/ugorji/go/codec v1.1.7
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.8
) )

30
go.sum
View File

@ -9,17 +9,17 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.0.1 h1:QgDDZpXlR/L3atIL2PbFt0TpazbtN7N6PxTGcgcyEUg= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.0.1/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
@ -34,11 +34,19 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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= 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/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)
}
}

View File

@ -2,6 +2,7 @@
// 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

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

@ -99,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
@ -141,7 +141,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// Truncate in a golang < 1.8 safe way // Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%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,
@ -275,11 +275,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))
} }
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
w := performRequest(router, "GET", "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String()) assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String()) assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestLoggerWithWriterSkippingPaths(t *testing.T) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {

View File

@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
} }

13
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
} }

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
@ -146,6 +166,6 @@ func function(pc uintptr) []byte {
} }
func timeFormat(t time.Time) string { func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05") timeString := t.Format("2006/01/02 - 15:04:05")
return timeString 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
@ -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,9 +41,6 @@ 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{}
@ -68,8 +66,11 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType. // WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
encoder := json.NewEncoder(w) jsonBytes, err := json.Marshal(obj)
err := encoder.Encode(&obj) if err != nil {
return err
}
_, err = w.Write(jsonBytes)
return err return err
} }
@ -97,8 +98,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
} }
@ -126,11 +128,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
} }
@ -138,7 +140,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
} }
@ -160,7 +162,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,10 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
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

@ -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(), string(buf.Bytes()))
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(), buf.String())
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{}{
@ -62,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }

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

@ -95,51 +95,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,7 +187,7 @@ 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)
} }

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:
@ -128,7 +128,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 +162,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,7 +356,7 @@ 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")
@ -371,13 +370,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 +385,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 +395,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 +441,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 +464,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 +488,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 +501,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 +512,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 +541,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 +561,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 +572,7 @@ 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())
} }
@ -569,6 +591,9 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
"/simple-two/one-two", "/simple-two/one-two",
"/project/:name/build/*params", "/project/:name/build/*params",
"/project/:name/bui", "/project/:name/bui",
"/user/:id/status",
"/user/:id",
"/user/:id/profile",
} }
for _, route := range routes { for _, route := range routes {
@ -581,7 +606,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
} }
for _, route := range routes { for _, route := range routes {
w := performRequest(router, "GET", route) w := performRequest(router, http.MethodGet, route)
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -591,6 +616,6 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
assert.Equal(t, "", c.FullPath()) assert.Equal(t, "", c.FullPath())
}) })
w := performRequest(router, "GET", "/not-found") w := performRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }

850
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,7 +40,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
} }
for _, request := range requests { for _, request := range requests {
value := tree.getValue(request.path, nil, unescape) value := tree.getValue(request.path, getParams(), unescape)
if value.handlers == nil { if value.handlers == nil {
if !request.nilHandler { if !request.nilHandler {
@ -50,9 +55,12 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
} }
} }
if !reflect.DeepEqual(value.params, 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) {
@ -152,6 +137,8 @@ func TestTreeWildcard(t *testing.T) {
"/", "/",
"/cmd/:tool/:sub", "/cmd/:tool/:sub",
"/cmd/:tool/", "/cmd/:tool/",
"/cmd/whoami",
"/cmd/whoami/root/",
"/src/*filepath", "/src/*filepath",
"/search/", "/search/",
"/search/:query", "/search/:query",
@ -170,8 +157,12 @@ 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/whoami", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/cmd/whoami", nil},
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/cmd/whoami/root", true, "/cmd/whoami/root/", nil},
{"/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"}}},
{"/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"}}},
@ -186,7 +177,6 @@ func TestTreeWildcard(t *testing.T) {
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func TestUnescapeParameters(t *testing.T) { func TestUnescapeParameters(t *testing.T) {
@ -224,7 +214,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 +251,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 +290,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 +333,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 +368,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 +382,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"
@ -501,6 +521,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 +547,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 +636,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 {
@ -666,8 +715,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 +737,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

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
} }

153
vendor/vendor.json vendored
View File

@ -1,153 +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": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
"path": "github.com/gin-contrib/sse",
"revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
"revisionTime": "2019-06-02T15:02:53Z",
"version": "v0.1",
"versionExact": "v0.1.0"
},
{
"checksumSHA1": "b4DmyMT9bicTRVJw1hJXHLhIH+0=",
"path": "github.com/go-playground/locales",
"revision": "f63010822830b6fe52288ee52d5a1151088ce039",
"revisionTime": "2018-03-23T16:04:04Z",
"version": "v0.12",
"versionExact": "v0.12.1"
},
{
"checksumSHA1": "JgF260rC9YpWyY5WEljjimWLUXs=",
"path": "github.com/go-playground/locales/currency",
"revision": "630ebbb602847eba93e75ae38bbc7bb7abcf1ff3",
"revisionTime": "2019-04-30T15:33:29Z"
},
{
"checksumSHA1": "9pKcUHBaVS+360X6h4IowhmOPjk=",
"path": "github.com/go-playground/universal-translator",
"revision": "b32fa301c9fe55953584134cb6853a13c87ec0a1",
"revisionTime": "2017-02-09T16:11:52Z",
"version": "v0.16",
"versionExact": "v0.16.0"
},
{
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
"path": "github.com/golang/protobuf/proto",
"revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
"revisionTime": "2019-02-05T22:20:52Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "zNo6yGy/bCJuzkEcP70oEBtOB2M=",
"path": "github.com/leodido/go-urn",
"revision": "70078a794e8ea4b497ba7c19a78cd60f90ccf0f4",
"revisionTime": "2018-05-24T03:26:21Z",
"version": "v1.1",
"versionExact": "v1.1.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": "wnEANt4k5X/KGwoFyfSSnpxULm4=",
"path": "github.com/stretchr/testify/require",
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
"revisionTime": "2018-05-06T18:05:49Z"
},
{
"checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
"path": "github.com/ugorji/go/codec",
"revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
"revisionTime": "2019-07-02T14:15:27Z",
"version": "v1.1",
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"path": "golang.org/x/sys/unix",
"revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
"revisionTime": "2019-05-02T15:41:39Z"
},
{
"checksumSHA1": "ACzc7AkwLtNgKhqtj8V7SGUJgnw=",
"path": "gopkg.in/go-playground/validator.v9",
"revision": "46b4b1e301c24cac870ffcb4ba5c8a703d1ef475",
"revisionTime": "2019-03-31T13:31:25Z",
"version": "v9.28",
"versionExact": "v9.28.0"
},
{
"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.5.0" const Version = "v1.7.0"