Merge branch 'master' into fix-cors

This commit is contained in:
unbyte 2021-11-02 15:12:48 +08:00 committed by GitHub
commit 51ef7a6a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 2834 additions and 686 deletions

View File

@ -1,7 +1,7 @@
- With pull requests:
- Open your pull request against `master`
- 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 integration systems such as TravisCI.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly

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

@ -0,0 +1,84 @@
name: Run Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.42.1
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: [1.13, 1.14, 1.15, 1.16, 1.17]
test-tags: ['', nomsgpack]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
- os: macos-latest
go-build: ~/Library/Caches/go-build
name: ${{ matrix.os }} @ Go ${{ matrix.go }} ${{ matrix.test-tags }}
runs-on: ${{ matrix.os }}
env:
GO111MODULE: on
TESTTAGS: ${{ matrix.test-tags }}
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout Code
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- uses: actions/cache@v2
with:
path: |
${{ matrix.go-build }}
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run Tests
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
notification-gitter:
needs: test
runs-on: ubuntu-latest
steps:
- name: Notification failure message
if: failure()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" -d level=error https://webhooks.gitter.im/e/7f95bf605c4d356372f4
- name: Notification success message
if: success()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" https://webhooks.gitter.im/e/7f95bf605c4d356372f4

39
.golangci.yml Normal file
View File

@ -0,0 +1,39 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- depguard
- dogsled
- durationcheck
- errcheck
- errorlint
- exportloopref
- gci
- gofmt
- goimports
- gosec
- misspell
- nakedret
- nilerr
- nolintlint
- revive
- wastedassign
issues:
exclude-rules:
- linters:
- structcheck
- unused
text: "`data` is unused"
- linters:
- staticcheck
text: "SA1019:"
- linters:
- revive
text: "var-naming:"
- linters:
- revive
text: "exported:"
- path: _test\.go
linters:
- gosec # security is not make sense in tests

View File

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

View File

@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
**@rw-access (Ross Wolf)**
- Added support to mix exact and param routes
**@se77en (Damon Zhao)**
- Improve color logging

View File

@ -1,5 +1,56 @@
# Gin ChangeLog
## Gin v1.7.3
### BUGFIXES
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
## Gin v1.7.2
### BUGFIXES
* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
## Gin v1.7.1
### BUGFIXES
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
## Gin v1.7.0
### BUGFIXES
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
### ENHANCEMENTS
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
* remove a unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
## Gin v1.6.3
### ENHANCEMENTS

View File

@ -8,6 +8,6 @@
- With pull requests:
- Open your pull request against `master`
- 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 integration systems such as TravisCI.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.

View File

@ -1,5 +1,6 @@
GO ?= go
GOFMT ?= gofmt "-s"
GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go")
@ -67,5 +68,10 @@ misspell:
.PHONY: tools
tools:
go install golang.org/x/lint/golint; \
go install github.com/client9/misspell/cmd/misspell;
@if [ $(GO_VERSION) -gt 15 ]; then \
$(GO) install golang.org/x/lint/golint@latest; \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
elif [ $(GO_VERSION) -lt 16 ]; then \
$(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \
fi

180
README.md
View File

@ -2,7 +2,7 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
[![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
[![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)
[![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
@ -23,7 +23,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Quick start](#quick-start)
- [Benchmarks](#benchmarks)
- [Gin v1. stable](#gin-v1-stable)
- [Build with jsoniter](#build-with-jsoniter)
- [Build with jsoniter/go-json](#build-with-json-replacement)
- [Build without `MsgPack` rendering feature](#build-without-msgpack-rendering-feature)
- [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)
@ -77,6 +78,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie)
- [Don't trust all proxies](#don't-trust-all-proxies)
- [Testing](#testing)
- [Users](#users)
@ -84,7 +86,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
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.13+ is required**), then you can use the below Go command to install Gin.
```sh
$ go get -u github.com/gin-gonic/gin
@ -178,17 +180,32 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- [x] Zero allocation router.
- [x] Still the fastest http router and framework. From routing to writing.
- [x] Complete suite of unit tests
- [x] Battle tested
- [x] Complete suite of unit tests.
- [x] Battle tested.
- [x] API frozen, new releases will not break your code.
## Build with [jsoniter](https://github.com/json-iterator/go)
## Build with json replacement
Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
Gin uses `encoding/json` as default json package but you can change it by build from other tags.
[jsoniter](https://github.com/json-iterator/go)
```sh
$ go build -tags=jsoniter .
```
[go-json](https://github.com/goccy/go-json)
```sh
$ go build -tags=go_json .
```
## Build without `MsgPack` rendering feature
Gin enables `MsgPack` rendering feature by default. But you can disable this feature by specifying `nomsgpack` build tag.
```sh
$ go build -tags=nomsgpack .
```
This is useful to reduce the binary size of executable files. See the [detail information](https://github.com/gin-gonic/gin/pull/1852).
## API Examples
@ -240,7 +257,15 @@ func main() {
// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
c.FullPath() == "/user/:name/*action" // true
b := c.FullPath() == "/user/:name/*action" // true
c.String(http.StatusOK, "%t", b)
})
// 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 [...]")
})
router.Run(":8080")
@ -340,7 +365,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
@ -679,7 +704,7 @@ func main() {
// Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>user</user>
// <user>manu</user>
// <password>123</password>
// </root>)
router.POST("/loginXML", func(c *gin.Context) {
@ -918,7 +943,7 @@ func main() {
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
c.JSON(400, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
@ -1255,6 +1280,7 @@ func main() {
}
reader := response.Body
defer reader.Close()
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
@ -1792,8 +1818,8 @@ func main() {
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Printf("listen: %s\n", err)
}
}()
@ -1802,7 +1828,7 @@ func main() {
quit := make(chan os.Signal)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// 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 caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
@ -1811,6 +1837,7 @@ func main() {
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
@ -1974,7 +2001,7 @@ func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This reads c.Request.Body and stores the result into the context.
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
if errA := c.ShouldBindBodyWith(&objA, binding.Form); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// At this time, it reuses body stored in the context.
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
@ -1996,6 +2023,61 @@ enough to call binding at once.
can be called by `c.ShouldBind()` multiple times without any damage to
performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
### Bind form-data request with custom struct and custom tag
```go
const (
customerTag = "url"
defaultMemory = 32 << 20
)
type customerBinding struct {}
func (customerBinding) Name() string {
return "form"
}
func (customerBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
if err := req.ParseMultipartForm(defaultMemory); err != nil {
if err != http.ErrNotMultipart {
return err
}
}
if err := binding.MapFormWithTag(obj, req.Form, customerTag); err != nil {
return err
}
return validate(obj)
}
func validate(obj interface{}) error {
if binding.Validator == nil {
return nil
}
return binding.Validator.ValidateStruct(obj)
}
// Now we can do this!!!
// FormA is a external type that we can't modify it's tag
type FormA struct {
FieldA string `url:"field_a"`
}
func ListHandler(s *Service) func(ctx *gin.Context) {
return func(ctx *gin.Context) {
var urlBinding = customerBinding{}
var opt FormA
err := ctx.MustBindWith(&opt, urlBinding)
if err != nil {
...
}
...
}
}
```
### http2 server push
http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
@ -2115,6 +2197,73 @@ 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.
Use function `SetTrustedProxies()` on your `gin.Engine` to specify 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.
**Attention:** Gin trust all proxies by default if you don't specify a trusted
proxy using the function above, **this is NOT safe**. At the same time, if you don't
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
then `Context.ClientIP()` will return the remote address directly to avoid some
unnecessary computation.
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.SetTrustedProxies([]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()
}
```
**Notice:** If you are using a CDN service, you can set the `Engine.TrustedPlatform`
to skip TrustedProxies check, it has a higher priority than TrustedProxies.
Look at the example below:
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Use predefined header gin.PlatformXXX
router.TrustedPlatform = gin.PlatformGoogleAppEngine
// Or set your own trusted request header for another trusted proxy service
// Don't set it to any suspect request header, it's unsafe
router.TrustedPlatform = "X-CDN-IP"
router.GET("/", func(c *gin.Context) {
// If you set TrustedPlatform, ClientIP() will resolve the
// corresponding header and return IP directly
fmt.Printf("ClientIP: %s\n", c.ClientIP())
})
router.Run()
}
```
## Testing
@ -2173,3 +2322,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.

View File

@ -5,6 +5,7 @@
package gin
import (
"crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
@ -30,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false
}
for _, pair := range a {
if pair.value == authValue {
if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true
}
}

View File

@ -2,6 +2,7 @@
// 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
@ -48,10 +49,11 @@ type BindingUri interface {
// 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.
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a 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 struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
@ -63,7 +65,7 @@ type StructValidator interface {
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}

View File

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

View File

@ -2,6 +2,7 @@
// 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
@ -46,7 +47,7 @@ type BindingUri interface {
// 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.
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
@ -61,7 +62,7 @@ type StructValidator interface {
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}

View File

@ -13,14 +13,15 @@ import (
"mime/multipart"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
type appkey struct {
@ -34,7 +35,7 @@ type QueryTest 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 {
@ -180,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"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) {
testBodyBindingUseNumber(t,
JSON, "json",
@ -200,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"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) {
testFormBinding(t, "POST",
"/", "/",
@ -336,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "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) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
@ -366,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"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) {
testBodyBinding(t,
XML, "xml",
@ -387,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
`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) {
testBodyBindingFail(t,
YAML, "yaml",
@ -751,7 +832,6 @@ func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, ba
assert.Equal(t, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
@ -1114,6 +1194,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
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) {
assert.Equal(t, name, b.Name())
@ -1219,6 +1339,13 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
err := b.Bind(req, &obj)
assert.Error(t, err)
invalid_obj := FooStruct{}
req.Body = ioutil.NopCloser(strings.NewReader(`{"msg":"hello"}`))
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = b.Bind(req, &invalid_obj)
assert.Error(t, err)
assert.Equal(t, err.Error(), "obj is not ProtoMessage")
obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
package binding
import (
"errors"
"net/http"
)
@ -22,11 +23,9 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
if err := req.ParseMultipartForm(defaultMemory); err != nil {
if err != http.ErrNotMultipart {
if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
return err
}
}
if err := mapForm(obj, req.Form); err != nil {
return err
}

View File

@ -16,9 +16,17 @@ import (
"github.com/gin-gonic/gin/internal/json"
)
var errUnknownType = errors.New("unknown type")
var (
errUnknownType = errors.New("unknown type")
func mapUri(ptr interface{}, m map[string][]string) error {
// ErrConvertMapStringSlice can not covert to map[string][]string
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
// ErrConvertToMapString can not convert to map[string]string
ErrConvertToMapString = errors.New("can not convert to map of strings")
)
func mapURI(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri")
}
@ -26,15 +34,34 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return mapFormByTag(ptr, form, "form")
}
func MapFormWithTag(ptr interface{}, form map[string][]string, tag string) error {
return mapFormByTag(ptr, form, tag)
}
var emptyField = reflect.StructField{}
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)
}
// setter tries to set value on a walking by fields of a struct
type setter interface {
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
type formSource map[string][]string
@ -42,7 +69,7 @@ type formSource map[string][]string
var _ setter = formSource(nil)
// TrySet tries to set a value by request's form source (like map[string][]string)
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {
return setByForm(value, field, form, tagValue, opt)
}
@ -56,7 +83,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return false, nil
}
var vKind = value.Kind()
vKind := value.Kind()
if vKind == reflect.Ptr {
var isNew bool
@ -65,14 +92,14 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
isSet, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
if isNew && isSetted {
if isNew && isSet {
value.Set(vPtr)
}
return isSetted, nil
return isSet, nil
}
if vKind != reflect.Struct || !field.Anonymous {
@ -88,19 +115,19 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if vKind == reflect.Struct {
tValue := value.Type()
var isSetted bool
var isSet bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil {
return false, err
}
isSetted = isSetted || ok
isSet = isSet || ok
}
return isSetted, nil
return isSet, nil
}
return false, nil
}
@ -137,7 +164,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
return setter.TrySet(value, field, tagValue, setOpt)
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
return false, nil
@ -183,7 +210,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case reflect.Int64:
switch value.Interface().(type) {
case time.Duration:
return setTimeDuration(val, value, field)
return setTimeDuration(val, value)
}
return setIntField(val, 64, value)
case reflect.Uint:
@ -283,7 +310,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" {
@ -333,7 +359,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
return nil
}
func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
func setTimeDuration(val string, value reflect.Value) error {
d, err := time.ParseDuration(val)
if err != nil {
return err
@ -349,3 +375,29 @@ func head(str, sep string) (head string, tail string) {
}
return str[:idx], str[idx+len(sep):]
}
func setFormMap(ptr interface{}, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string)
if !ok {
return ErrConvertMapStringSlice
}
for k, v := range form {
ptrMap[k] = v
}
return nil
}
ptrMap, ok := ptr.(map[string]string)
if !ok {
return ErrConvertToMapString
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
return nil
}

View File

@ -131,7 +131,7 @@ func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
err := mapUri(&s, map[string][]string{"field": {"6"}})
err := mapURI(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
@ -145,6 +145,15 @@ func TestMappingForm(t *testing.T) {
assert.Equal(t, int(6), s.F)
}
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingTime(t *testing.T) {
var s struct {
Time time.Time

View File

@ -29,6 +29,6 @@ type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -6,7 +6,7 @@ package binding
import (
"bytes"
"fmt"
"errors"
"io"
"net/http"
@ -32,7 +32,7 @@ func (jsonBinding) Name() string {
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
return errors.New("invalid request")
}
return decodeJSON(req.Body, obj)
}

View File

@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func TestJSONBindingBindBodyMap(t *testing.T) {
s := make(map[string]string)
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
require.NoError(t, err)
assert.Len(t, s, 2)
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}

View File

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

View File

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

View File

@ -15,8 +15,16 @@ type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
// ErrMultiFileHeader multipart.FileHeader invalid
ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader")
// ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid
ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*multipart.FileHeader")
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (bool, error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
@ -24,7 +32,7 @@ func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
@ -40,26 +48,26 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
isSet, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSet {
return isSet, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
return false, ErrMultiFileHeader
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
return false, ErrMultiFileHeaderLenInvalid
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !set {
return set, err
}
}
return true, nil

View File

@ -124,7 +124,7 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open()
assert.NoError(t, err)

View File

@ -5,10 +5,11 @@
package binding
import (
"errors"
"io/ioutil"
"net/http"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
type protobufBinding struct{}
@ -26,7 +27,11 @@ func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
}
func (protobufBinding) BindBody(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
msg, ok := obj.(proto.Message)
if !ok {
return errors.New("obj is not ProtoMessage")
}
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add

View File

@ -11,7 +11,7 @@ func (uriBinding) Name() string {
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
if err := mapURI(obj, m); err != nil {
return err
}
return validate(obj)

View File

@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"math"
"mime/multipart"
"net"
@ -39,7 +40,8 @@ const (
// BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
const abortIndex int8 = math.MaxInt8 / 2
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
@ -55,6 +57,7 @@ type Context struct {
engine *Engine
params *Params
skippedNodes *[]skippedNode
// This mutex protect Keys map
mu sync.RWMutex
@ -86,17 +89,18 @@ type Context struct {
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.Params = c.Params[:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Errors = c.Errors[:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
*c.params = (*c.params)[0:0]
*c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0]
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
@ -218,7 +222,8 @@ func (c *Context) Error(err error) *Error {
panic("err is nil")
}
parsedError, ok := err.(*Error)
var parsedError *Error
ok := errors.As(err, &parsedError)
if !ok {
parsedError = &Error{
Err: err,
@ -295,6 +300,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
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.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
@ -365,6 +386,15 @@ func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
// AddParam adds param to context and
// replaces path param key with given value for e2e testing purposes
// Example Route: "/user/:id"
// AddParam("id", 1)
// Result: "/user/1"
func (c *Context) AddParam(key, value string) {
c.Params = append(c.Params, Param{Key: key, Value: value})
}
// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
@ -373,9 +403,9 @@ func (c *Context) Param(key string) string {
// c.Query("name") == "Manu"
// c.Query("value") == ""
// c.Query("wtf") == ""
func (c *Context) Query(key string) string {
value, _ := c.GetQuery(key)
return value
func (c *Context) Query(key string) (value string) {
value, _ = c.GetQuery(key)
return
}
// DefaultQuery returns the keyed url query value if it exists,
@ -409,9 +439,9 @@ func (c *Context) GetQuery(key string) (string, bool) {
// QueryArray returns a slice of strings for a given query key.
// The length of the slice depends on the number of params with the given key.
func (c *Context) QueryArray(key string) []string {
values, _ := c.GetQueryArray(key)
return values
func (c *Context) QueryArray(key string) (values []string) {
values, _ = c.GetQueryArray(key)
return
}
func (c *Context) initQueryCache() {
@ -426,18 +456,16 @@ func (c *Context) initQueryCache() {
// 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.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
c.initQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
return []string{}, false
values, ok = c.queryCache[key]
return
}
// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
dicts, _ := c.GetQueryMap(key)
return dicts
func (c *Context) QueryMap(key string) (dicts map[string]string) {
dicts, _ = c.GetQueryMap(key)
return
}
// GetQueryMap returns a map for a given query key, plus a boolean value
@ -449,9 +477,9 @@ func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
value, _ := c.GetPostForm(key)
return value
func (c *Context) PostForm(key string) (value string) {
value, _ = c.GetPostForm(key)
return
}
// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
@ -480,9 +508,9 @@ func (c *Context) GetPostForm(key string) (string, bool) {
// PostFormArray returns a slice of strings for a given form key.
// The length of the slice depends on the number of params with the given key.
func (c *Context) PostFormArray(key string) []string {
values, _ := c.GetPostFormArray(key)
return values
func (c *Context) PostFormArray(key string) (values []string) {
values, _ = c.GetPostFormArray(key)
return
}
func (c *Context) initFormCache() {
@ -490,7 +518,7 @@ func (c *Context) initFormCache() {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
if !errors.Is(err, http.ErrNotMultipart) {
debugPrint("error on parse multipart form array: %v", err)
}
}
@ -500,18 +528,16 @@ func (c *Context) initFormCache() {
// 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.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
c.initFormCache()
if values := c.formCache[key]; len(values) > 0 {
return values, true
}
return []string{}, false
values, ok = c.formCache[key]
return
}
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
func (c *Context) PostFormMap(key string) (dicts map[string]string) {
dicts, _ = c.GetPostFormMap(key)
return
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
@ -709,32 +735,91 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
// ClientIP implements one best effort algorithm to return the real client IP.
// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming form Request.RemoteAddr) is returned.
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
// Check if we're running on a trusted platform, continue running backwards if error
if c.engine.TrustedPlatform != "" {
// Developers can define their own header of Trusted Platform or use predefined constants
if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
return addr
}
}
// Legacy "AppEngine" flag
if c.engine.AppEngine {
log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`)
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip
remoteIP, trusted := c.RemoteIP()
if remoteIP == nil {
return ""
}
return ""
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
if valid {
return ip
}
}
}
return remoteIP.String()
}
func (e *Engine) isTrustedProxy(ip net.IP) bool {
if e.trustedCIDRs != nil {
for _, cidr := range e.trustedCIDRs {
if cidr.Contains(ip) {
return true
}
}
}
return false
}
// 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 by Engine.SetTrustedProxies()
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
}
return remoteIP, c.engine.isTrustedProxy(remoteIP)
}
func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
if ip == nil {
return "", false
}
// X-Forwarded-For is appended by proxy
// Check IPs in reverse order and stop when find untrusted proxy
if (i == 0) || (!e.isTrustedProxy(ip)) {
return ipStr, true
}
}
return
}
// ContentType returns the Content-Type header of the request.
@ -875,7 +960,7 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
}
// 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".
func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "")
@ -952,12 +1037,12 @@ 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) {
http.ServeFile(c.Writer, c.Request, filepath)
}
// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way.
// 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
@ -971,7 +1056,7 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
// 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
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)
}
@ -1081,22 +1166,28 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
// Deadline always returns that there is no deadline (ok==false),
// maybe you want to use Request.Context().Deadline() instead.
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
if c.Request == nil || c.Request.Context() == nil {
return
}
return c.Request.Context().Deadline()
}
// Done always returns nil (chan which will wait forever),
// if you want to abort your work when the connection was closed
// you should use Request.Context().Done() instead.
// Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} {
if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Done()
}
// Err always returns nil, maybe you want to use Request.Context().Err() instead.
// Err returns nil when c.Request has no Context.
func (c *Context) Err() error {
if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Err()
}
// Value returns the value associated with this context for key, or nil
@ -1107,8 +1198,12 @@ func (c *Context) Value(key interface{}) interface{} {
return c.Request
}
if keyAsString, ok := key.(string); ok {
val, _ := c.Get(keyAsString)
if val, exists := c.Get(keyAsString); exists {
return val
}
}
if c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Value(key)
}

31
context_1.16_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2021 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 !go1.17
// +build !go1.17
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed16(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}

33
context_1.17_test.go Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2021 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 go1.17
// +build go1.17
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed17(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
assert.Panics(t, func() {
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
})
}

View File

@ -1,11 +1,12 @@
// +build appengine
// Copyright 2017 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 appengine
// +build appengine
package gin
func init() {
defaultAppEngine = true
defaultPlatform = PlatformGoogleAppEngine
}

View File

@ -23,10 +23,9 @@ import (
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
var _ context.Context = &Context{}
@ -87,19 +86,6 @@ func TestContextFormFile(t *testing.T) {
assert.NoError(t, c.SaveUploadedFile(f, "test"))
}
func TestContextFormFileFailed(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
@ -234,7 +220,6 @@ func TestContextSetGetValues(t *testing.T) {
assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2))
assert.Exactly(t, c.MustGet("float64").(float64), 4.2)
assert.Exactly(t, c.MustGet("intInterface").(int), 1)
}
func TestContextGetString(t *testing.T) {
@ -261,6 +246,18 @@ func TestContextGetInt64(t *testing.T) {
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) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2)
@ -288,7 +285,7 @@ func TestContextGetStringSlice(t *testing.T) {
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]interface{})
m := make(map[string]interface{})
m["foo"] = 1
c.Set("map", m)
@ -298,7 +295,7 @@ func TestContextGetStringMap(t *testing.T) {
func TestContextGetStringMapString(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]string)
m := make(map[string]string)
m["foo"] = "bar"
c.Set("map", m)
@ -308,7 +305,7 @@ func TestContextGetStringMapString(t *testing.T) {
func TestContextGetStringMapStringSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string][]string)
m := make(map[string][]string)
m["foo"] = []string{"foo"}
c.Set("map", m)
@ -357,15 +354,12 @@ func TestContextHandlerNames(t *testing.T) {
}
func handlerNameTest(c *Context) {
}
func handlerNameTest2(c *Context) {
}
var handlerTest HandlerFunc = func(c *Context) {
}
func TestContextHandler(t *testing.T) {
@ -647,8 +641,7 @@ func TestContextBodyAllowedForStatus(t *testing.T) {
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
}
type TestPanicRender struct {
}
type TestPanicRender struct{}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
@ -1006,7 +999,9 @@ func TestContextRenderFile(t *testing.T) {
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"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
}
func TestContextRenderFileFromFS(t *testing.T) {
@ -1018,7 +1013,9 @@ func TestContextRenderFileFromFS(t *testing.T) {
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"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
}
@ -1032,7 +1029,7 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
@ -1270,7 +1267,7 @@ func TestContextIsAborted(t *testing.T) {
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
func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder()
@ -1313,7 +1310,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
assert.Equal(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
}
func TestContextError(t *testing.T) {
@ -1379,12 +1376,11 @@ func TestContextAbortWithError(t *testing.T) {
func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
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())
c.Request.Header.Del("X-Forwarded-For")
@ -1395,7 +1391,7 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true
c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
@ -1404,6 +1400,107 @@ func TestContextClientIP(t *testing.T) {
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
// Tests exercising the TrustedProxies functionality
resetContextForClientIPTests(c)
// No trusted proxies
_ = c.engine.SetTrustedProxies([]string{})
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Disabled TrustedProxies feature
_ = c.engine.SetTrustedProxies(nil)
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Last proxy is trusted, but the RemoteAddr is not
_ = c.engine.SetTrustedProxies([]string{"30.30.30.30"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
assert.Equal(t, "30.30.30.30", c.ClientIP())
// All steps are trusted
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30", "20.20.20.20"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use CIDR
_ = c.engine.SetTrustedProxies([]string{"40.40.25.25/16", "30.30.30.30"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use hostname that resolves to all the proxies
_ = c.engine.SetTrustedProxies([]string{"foo"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
_ = c.engine.SetTrustedProxies([]string{"bar"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", " blah ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Result from LookupHost has non-IP element. This should never
// happen, but we should test it to make sure we handle it
// gracefully.
_ = c.engine.SetTrustedProxies([]string{"baz"})
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Del("X-Forwarded-For")
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.engine.RemoteIPHeaders = []string{}
c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
// Use custom TrustedPlatform header
c.engine.TrustedPlatform = "X-CDN-IP"
c.Request.Header.Set("X-CDN-IP", "80.80.80.80")
assert.Equal(t, "80.80.80.80", c.ClientIP())
// wrong header
c.engine.TrustedPlatform = "X-Wrong-Header"
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.Request.Header.Del("X-CDN-IP")
// TrustedPlatform is empty
c.engine.TrustedPlatform = ""
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Test the legacy flag
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.engine.AppEngine = false
c.engine.TrustedPlatform = PlatformGoogleAppEngine
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = PlatformCloudflare
assert.Equal(t, "60.60.60.60", c.ClientIP())
c.Request.Header.Del("CF-Connecting-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = ""
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
}
func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.TrustedPlatform = ""
c.engine.AppEngine = false
}
func TestContextContentType(t *testing.T) {
@ -1445,6 +1542,7 @@ func TestContextBindWithJSON(t *testing.T) {
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1948,3 +2046,120 @@ func TestContextWithKeysMutex(t *testing.T) {
assert.Nil(t, value)
assert.False(t, err)
}
func TestRemoteIPFail(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.RemoteAddr = "[:::]:80"
ip, trust := c.RemoteIP()
assert.Nil(t, ip)
assert.False(t, trust)
}
func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) {
c := &Context{}
deadline, ok := c.Deadline()
assert.Zero(t, deadline)
assert.False(t, ok)
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
d := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
c2.Request = c2.Request.WithContext(ctx)
deadline, ok = c2.Deadline()
assert.Equal(t, d, deadline)
assert.True(t, ok)
}
func TestContextWithFallbackDoneFromRequestContext(t *testing.T) {
c := &Context{}
assert.Nil(t, c.Done())
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.NotNil(t, <-c2.Done())
}
func TestContextWithFallbackErrFromRequestContext(t *testing.T) {
c := &Context{}
assert.Nil(t, c.Err())
c2 := &Context{}
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.EqualError(t, c2.Err(), context.Canceled.Error())
}
type contextKey string
func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
tests := []struct {
name string
getContextAndKey func() (*Context, interface{})
value interface{}
}{
{
name: "c with struct context key",
getContextAndKey: func() (*Context, interface{}) {
var key struct{}
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "value"))
return c, key
},
value: "value",
},
{
name: "c with string context key",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), contextKey("key"), "value"))
return c, contextKey("key")
},
value: "value",
},
{
name: "c with nil http.Request",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
return c, "key"
},
value: nil,
},
{
name: "c with nil http.Request.Context()",
getContextAndKey: func() (*Context, interface{}) {
c := &Context{}
c.Request, _ = http.NewRequest("POST", "/", nil)
return c, "key"
},
value: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, key := tt.getContextAndKey()
assert.Equal(t, tt.value, c.Value(key))
})
}
}
func TestContextAddParam(t *testing.T) {
c := &Context{}
id := "id"
value := "1"
c.AddParam(id, value)
v, ok := c.Params.Get(id)
assert.Equal(t, ok, true)
assert.Equal(t, value, v)
}

View File

@ -12,7 +12,7 @@ import (
"strings"
)
const ginSupportMinGoVer = 10
const ginSupportMinGoVer = 13
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
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.13+.
`)
}
@ -95,9 +95,7 @@ at initialization. ie. before any route is registered or the router is listening
}
func debugPrintError(err error) {
if err != nil {
if IsDebugging() {
if err != nil && IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
}

View File

@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
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.13+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}

View File

@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
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.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@ -117,7 +122,7 @@ func (a errorMsgs) Last() *Error {
return nil
}
// Errors returns an array will all the error messages.
// Errors returns an array with all the error messages.
// Example:
// c.Error(errors.New("first"))
// c.Error(errors.New("second"))

View File

@ -6,6 +6,7 @@ package gin
import (
"errors"
"fmt"
"testing"
"github.com/gin-gonic/gin/internal/json"
@ -104,3 +105,24 @@ Error #03: third
assert.Nil(t, errs.JSON())
assert.Empty(t, errs.String())
}
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("some 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))
}

2
fs.go
View File

@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
http.File
}
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.

159
gin.go
View File

@ -11,6 +11,8 @@ import (
"net/http"
"os"
"path"
"reflect"
"strings"
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
@ -24,7 +26,9 @@ var (
default405Body = []byte("405 method not allowed")
)
var defaultAppEngine bool
var defaultPlatform string
var defaultTrustedCIDRs = []*net.IPNet{{IP: net.IP{0x0, 0x0, 0x0, 0x0}, Mask: net.IPMask{0x0, 0x0, 0x0, 0x0}}} // 0.0.0.0/0
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@ -51,6 +55,16 @@ type RouteInfo struct {
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo
// Trusted platforms
const (
// When running on Google App Engine. Trust X-Appengine-Remote-Addr
// for determining the client's IP
PlatformGoogleAppEngine = "X-Appengine-Remote-Addr"
// When using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP
PlatformCloudflare = "CF-Connecting-IP"
)
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
@ -81,9 +95,15 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
// 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
// #726 #755 If enabled, it will thrust some headers starting with
// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
// #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
@ -95,14 +115,24 @@ type Engine struct {
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call.
MaxMultipartMemory int64
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
// List of headers used to obtain the client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
RemoteIPHeaders []string
// If set to a constant of value gin.Platform*, trusts the headers set by
// that platform, for example to determine the client IP
TrustedPlatform string
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call.
MaxMultipartMemory int64
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
@ -116,6 +146,9 @@ type Engine struct {
pool sync.Pool
trees methodTrees
maxParams uint16
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
}
var _ IRouter = &Engine{}
@ -141,7 +174,8 @@ func New() *Engine {
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
@ -149,6 +183,8 @@ func New() *Engine {
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
@ -167,7 +203,8 @@ func Default() *Engine {
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
return &Context{engine: engine, params: &v}
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
// Delims sets template left and right delims and returns a Engine instance.
@ -230,7 +267,7 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.rebuild404Handlers()
}
// NoMethod sets the handlers called when... TODO.
// NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.noMethod = handlers
engine.rebuild405Handlers()
@ -285,6 +322,10 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
// Routes returns a slice of registered routes, including some useful information, such as:
@ -319,12 +360,85 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil {
return nil, nil
}
cidr := make([]*net.IPNet, 0, len(engine.trustedProxies))
for _, trustedProxy := range engine.trustedProxies {
if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy)
if ip == nil {
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
}
switch len(ip) {
case net.IPv4len:
trustedProxy += "/32"
case net.IPv6len:
trustedProxy += "/128"
}
}
_, cidrNet, err := net.ParseCIDR(trustedProxy)
if err != nil {
return cidr, err
}
cidr = append(cidr, cidrNet)
}
return cidr, nil
}
// SetTrustedProxies set a 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`
// feature is enabled by default, and it also trusts all proxies
// by default. If you want to disable this feature, use
// Engine.SetTrustedProxies(nil), then Context.ClientIP() will
// return the remote address directly.
func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
engine.trustedProxies = trustedProxies
return engine.parseTrustedProxies()
}
// isUnsafeTrustedProxies compares Engine.trustedCIDRs and defaultTrustedCIDRs, it's not safe if equal (returns true)
func (engine *Engine) isUnsafeTrustedProxies() bool {
return reflect.DeepEqual(engine.trustedCIDRs, defaultTrustedCIDRs)
}
// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
func (engine *Engine) parseTrustedProxies() error {
trustedCIDRs, err := engine.prepareTrustedCIDRs()
engine.trustedCIDRs = trustedCIDRs
return err
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
parsedIP := net.ParseIP(ip)
if ipv4 := parsedIP.To4(); ipv4 != nil {
// return ip in a 4-byte representation
return ipv4
}
// return ip in a 16-byte representation or nil
return parsedIP
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -332,6 +446,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
return
}
@ -343,6 +462,11 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
listener, err := net.Listen("unix", file)
if err != nil {
return
@ -361,6 +485,11 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f)
if err != nil {
@ -376,6 +505,12 @@ func (engine *Engine) RunFd(fd int) (err error) {
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
err = http.Serve(listener, engine)
return
}
@ -424,7 +559,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
@ -435,7 +570,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if httpMethod != http.MethodConnect && rPath != "/" {
c.handlers = engine.allAutoRedirect
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
@ -453,7 +588,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return

View File

@ -14,6 +14,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
@ -21,7 +22,15 @@ import (
"github.com/stretchr/testify/assert"
)
func testRequest(t *testing.T, url string) {
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) {
if len(params) == 0 {
t.Fatal("url cannot be empty")
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
@ -29,14 +38,27 @@ func testRequest(t *testing.T, url string) {
}
client := &http.Client{Transport: tr}
resp, err := client.Get(url)
resp, err := client.Get(params[0])
assert.NoError(t, err)
defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body)
assert.NoError(t, ioerr)
assert.Equal(t, "it worked", string(body), "resp body should match")
assert.Equal(t, "200 OK", resp.Status, "should get a 200")
var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
var responseBody = "it worked"
if len(params) > 2 && params[2] != "" {
responseBody = params[2]
}
assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
if responseStatus == "200 OK" {
assert.Equal(t, responseBody, string(body), "resp body should match")
}
}
func TestRunEmpty(t *testing.T) {
@ -54,6 +76,81 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example")
}
func TestBadTrustedCIDRs(t *testing.T) {
router := New()
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
}
/* legacy tests
func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
}
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}
*/
func TestRunTLS(t *testing.T) {
router := New()
go func() {
@ -146,7 +243,7 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) {
router := New()
unixTestSocket := "/tmp/unix_unit_test"
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
@ -304,3 +401,149 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
func TestTreeRunDynamicRouting(t *testing.T) {
router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
router.GET("/c1/:dd/e", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e") })
router.GET("/c1/:dd/e1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e1") })
router.GET("/c1/:dd/f1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f1") })
router.GET("/c1/:dd/f2", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f2") })
router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
router.GET("/:cc/:dd/f", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/f") })
router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
router.GET("/get/abc", func(c *Context) { c.String(http.StatusOK, "/get/abc") })
router.GET("/get/:param", func(c *Context) { c.String(http.StatusOK, "/get/:param") })
router.GET("/get/abc/123abc", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc") })
router.GET("/get/abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param") })
router.GET("/get/abc/123abc/xxx8", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8") })
router.GET("/get/abc/123abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/:param") })
router.GET("/get/abc/123abc/xxx8/1234", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234") })
router.GET("/get/abc/123abc/xxx8/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/:param") })
router.GET("/get/abc/123abc/xxx8/1234/ffas", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/ffas") })
router.GET("/get/abc/123abc/xxx8/1234/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/:param") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/12c", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/12c") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/:param") })
router.GET("/get/abc/:param/test", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param/test") })
router.GET("/get/abc/123abd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abd/:param") })
router.GET("/get/abc/123abddd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abddd/:param") })
router.GET("/get/abc/123/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123/:param") })
router.GET("/get/abc/123abg/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abg/:param") })
router.GET("/get/abc/123abf/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abf/:param") })
router.GET("/get/abc/123abfff/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abfff/:param") })
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/", "", "home")
testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
testRequest(t, ts.URL+"/all", "", "/:cc")
testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/c1/d/e", "", "/c1/:dd/e")
testRequest(t, ts.URL+"/c1/d/e1", "", "/c1/:dd/e1")
testRequest(t, ts.URL+"/c1/d/ee", "", "/:cc/:dd/ee")
testRequest(t, ts.URL+"/c1/d/f", "", "/:cc/:dd/f")
testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/a", "", "/:cc")
testRequest(t, ts.URL+"/d", "", "/:cc")
testRequest(t, ts.URL+"/ad", "", "/:cc")
testRequest(t, ts.URL+"/dd", "", "/:cc")
testRequest(t, ts.URL+"/aa", "", "/:cc")
testRequest(t, ts.URL+"/aaa", "", "/:cc")
testRequest(t, ts.URL+"/aaa/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ab", "", "/:cc")
testRequest(t, ts.URL+"/abb", "", "/:cc")
testRequest(t, ts.URL+"/abb/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/dddaa", "", "/:cc")
testRequest(t, ts.URL+"/allxxxx", "", "/:cc")
testRequest(t, ts.URL+"/alldd", "", "/:cc")
testRequest(t, ts.URL+"/cc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ccc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/deedwjfs/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/acllcc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
testRequest(t, ts.URL+"/get/testaa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/get/abc", "", "/get/abc")
testRequest(t, ts.URL+"/get/a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abz", "", "/get/:param")
testRequest(t, ts.URL+"/get/12a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abcd", "", "/get/:param")
testRequest(t, ts.URL+"/get/abc/123abc", "", "/get/abc/123abc")
testRequest(t, ts.URL+"/get/abc/12", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123ab", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/xyz", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abcddxx", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8", "", "/get/abc/123abc/xxx8")
testRequest(t, ts.URL+"/get/abc/123abc/x", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/abc", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8xxas", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234", "", "/get/abc/123abc/xxx8/1234")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/123", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/78k", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234xxxd", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas", "", "/get/abc/123abc/xxx8/1234/ffas")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/f", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffa", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kka", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas321", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c", "", "/get/abc/123abc/xxx8/1234/kkdd/12c")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/1", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12b", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/34", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/12/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdd/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdddf/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123ab/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abgg/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abffff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abd/test", "", "/get/abc/123abd/:param")
testRequest(t, ts.URL+"/get/abc/123abddd/test", "", "/get/abc/123abddd/:param")
testRequest(t, ts.URL+"/get/abc/123/test22", "", "/get/abc/123/:param")
testRequest(t, ts.URL+"/get/abc/123abg/test", "", "/get/abc/123abg/:param")
testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param")
testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param")
// 404 not found
testRequest(t, ts.URL+"/c/d/e", "404 Not Found")
testRequest(t, ts.URL+"/c/d/e1", "404 Not Found")
testRequest(t, ts.URL+"/c/d/eee", "404 Not Found")
testRequest(t, ts.URL+"/c1/d/eee", "404 Not Found")
testRequest(t, ts.URL+"/c1/d/e2", "404 Not Found")
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh1", "404 Not Found")
testRequest(t, ts.URL+"/a/dd", "404 Not Found")
testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
}

View File

@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"reflect"
@ -585,6 +586,120 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
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")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv4 cidr
{
err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
assert.Error(t, err)
}
// valid ipv4 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
err := r.SetTrustedProxies([]string{"192.168.1.33"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv4 address
{
err := r.SetTrustedProxies([]string{"192.168.1.256"})
assert.Error(t, err)
}
// valid ipv6 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv6 address
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.Error(t, err)
}
// valid ipv6 cidr
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv6 cidr
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
assert.Error(t, err)
}
// valid combination
{
expectedTrustedCIDRs := []*net.IPNet{
parseCIDR("::/0"),
parseCIDR("192.168.0.0/16"),
parseCIDR("172.16.0.1/32"),
}
err := r.SetTrustedProxies([]string{
"::/0",
"192.168.0.0/16",
"172.16.0.1",
})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid combination
{
err := r.SetTrustedProxies([]string{
"::/0",
"192.168.0.0/16",
"172.16.0.256",
})
assert.Error(t, err)
}
// nil value
{
err := r.SetTrustedProxies(nil)
assert.Nil(t, r.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) {
for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

15
go.mod
View File

@ -4,11 +4,12 @@ go 1.13
require (
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
gopkg.in/yaml.v2 v2.2.8
github.com/go-playground/validator/v10 v10.9.0
github.com/goccy/go-json v0.7.10
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.14
github.com/stretchr/testify v1.7.0
github.com/ugorji/go/codec v1.2.6
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0
)

94
go.sum
View File

@ -1,3 +1,4 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -5,41 +6,76 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
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/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec=
github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,16 +5,17 @@
package bytesconv
import (
"reflect"
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
return b
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.

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

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

View File

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

View File

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

View File

@ -138,8 +138,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
}
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
param.Latency = param.Latency.Truncate(time.Second)
}
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"),

View File

@ -185,6 +185,8 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {

View File

@ -118,7 +118,10 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
func TestMiddlewareNoMethodDisabled(t *testing.T) {
signature := ""
router := New()
// NoMethod disabled
router.HandleMethodNotAllowed = false
router.Use(func(c *Context) {
signature += "A"
c.Next()
@ -144,6 +147,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := performRequest(router, "GET", "/")

View File

@ -41,8 +41,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
var ginMode = debugCode
var modeName = DebugMode
var (
ginMode = debugCode
modeName = DebugMode
)
func init() {
mode := os.Getenv(EnvGinMode)
@ -63,7 +65,7 @@ func SetMode(value string) {
case TestMode:
ginMode = testCode
default:
panic("gin mode unknown: " + value)
panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
modeName = value

View File

@ -6,6 +6,7 @@ package gin
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@ -34,7 +35,7 @@ func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
//CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
// 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)
}
@ -60,7 +61,8 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
var se *os.SyscallError
if errors.As(ne, &se) {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
@ -76,11 +78,12 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
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 {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
@ -164,7 +167,7 @@ func function(pc uintptr) []byte {
return name
}
// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string {
timeString := t.Format("2006/01/02 - 15:04:05")
return timeString
return t.Format("2006/01/02 - 15:04:05")
}

View File

@ -92,14 +92,14 @@ func TestPanicWithAbort(t *testing.T) {
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
@ -107,7 +107,7 @@ func TestSource(t *testing.T) {
func TestFunction(t *testing.T) {
bs := function(1)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
}
// TestPanicWithBrokenPipe asserts that recovery specifically handles

View File

@ -46,9 +46,11 @@ type PureJSON struct {
Data interface{}
}
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
var (
jsonContentType = []string{"application/json; charset=utf-8"}
jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonASCIIContentType = []string{"application/json"}
)
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
@ -100,8 +102,7 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
// if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
bytesconv.StringToBytes("]")) {
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil {
return err
}
}
@ -128,20 +129,19 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
}
callback := template.JSEscapeString(r.Callback)
_, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
return err
}
_, err = w.Write(bytesconv.StringToBytes("("))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
return err
}
_, err = w.Write(ret)
if err != nil {
if _, err = w.Write(ret); err != nil {
return err
}
_, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
return err
}
@ -176,7 +176,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
// WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
writeContentType(w, jsonASCIIContentType)
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.

View File

@ -2,6 +2,7 @@
// 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
@ -12,6 +13,8 @@ import (
"github.com/ugorji/go/codec"
)
// Check interface implemented here to support go build tag nomsgpack.
// See: https://github.com/gin-gonic/gin/pull/1852/
var (
_ Render = MsgPack{}
)

View File

@ -7,7 +7,7 @@ package render
import (
"net/http"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
// ProtoBuf contains the given interface object.

View File

@ -2,6 +2,7 @@
// 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
@ -38,6 +39,6 @@ func TestRenderMsgPack(t *testing.T) {
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -14,10 +14,9 @@ import (
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
// TODO unit tests
@ -420,7 +419,8 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
@ -438,7 +438,8 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: nil,
htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
@ -455,7 +456,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
}
func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{Files: nil,
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
Delims: Delims{"{{", "}}"},
FuncMap: nil,

View File

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

View File

@ -17,12 +17,14 @@ import (
// func (w *responseWriter) CloseNotify() <-chan bool {
// func (w *responseWriter) Flush() {
var _ ResponseWriter = &responseWriter{}
var _ http.ResponseWriter = &responseWriter{}
var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
var _ http.Hijacker = ResponseWriter(&responseWriter{})
var _ http.Flusher = ResponseWriter(&responseWriter{})
var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
var (
_ ResponseWriter = &responseWriter{}
_ http.ResponseWriter = &responseWriter{}
_ http.ResponseWriter = ResponseWriter(&responseWriter{})
_ http.Hijacker = ResponseWriter(&responseWriter{})
_ http.Flusher = ResponseWriter(&responseWriter{})
_ http.CloseNotifier = ResponseWriter(&responseWriter{})
)
func init() {
SetMode(TestMode)

View File

@ -11,6 +11,18 @@ import (
"strings"
)
var (
// reg match english letters for http method name
regEnLetter = regexp.MustCompile("^[A-Z]+$")
// anyMethods for RouterGroup Any method
anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}
)
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
IRoutes
@ -87,7 +99,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid")
}
return group.handle(httpMethod, relativePath, handlers)
@ -131,15 +143,10 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRo
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
group.handle(http.MethodGet, relativePath, handlers)
group.handle(http.MethodPost, relativePath, handlers)
group.handle(http.MethodPut, relativePath, handlers)
group.handle(http.MethodPatch, relativePath, handlers)
group.handle(http.MethodHead, relativePath, handlers)
group.handle(http.MethodOptions, relativePath, handlers)
group.handle(http.MethodDelete, relativePath, handlers)
group.handle(http.MethodConnect, relativePath, handlers)
group.handle(http.MethodTrace, relativePath, handlers)
for _, method := range anyMethods {
group.handle(method, relativePath, handlers)
}
return group.returnObj()
}
@ -209,9 +216,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)

View File

@ -112,15 +112,19 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
}
func TestRouterGroupTooManyHandlers(t *testing.T) {
const (
panicValue = "too many handlers"
maximumCnt = abortIndex
)
router := New()
handlers1 := make([]HandlerFunc, 40)
handlers1 := make([]HandlerFunc, maximumCnt-1)
router.Use(handlers1...)
handlers2 := make([]HandlerFunc, 26)
assert.Panics(t, func() {
handlers2 := make([]HandlerFunc, maximumCnt+1)
assert.PanicsWithValue(t, panicValue, func() {
router.Use(handlers2...)
})
assert.Panics(t, func() {
assert.PanicsWithValue(t, panicValue, func() {
router.GET("/", handlers2...)
})
}

View File

@ -259,7 +259,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@ -293,7 +292,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@ -383,7 +381,9 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@ -502,6 +502,21 @@ func TestRouterNotFound(t *testing.T) {
router.GET("/a", func(c *Context) {})
w = performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
// Reproduction test for the bug of issue #2843
router = New()
router.NoRoute(func(c *Context) {
if c.Request.RequestURI == "/login" {
c.String(200, "login")
}
})
router.GET("/logout", func(c *Context) {
c.String(200, "logout")
})
w = performRequest(router, http.MethodGet, "/login")
assert.Equal(t, "login", w.Body.String())
w = performRequest(router, http.MethodGet, "/logout")
assert.Equal(t, "logout", w.Body.String())
}
func TestRouterStaticFSNotFound(t *testing.T) {

View File

@ -1,24 +1,24 @@
// Code generated by protoc-gen-go.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.0
// protoc v3.15.8
// source: test.proto
// DO NOT EDIT!
/*
Package protoexample is a generated protocol buffer package.
It is generated from these files:
test.proto
It has these top-level messages:
Test
*/
package protoexample
import proto "github.com/golang/protobuf/proto"
import math "math"
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = math.Inf
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type FOO int32
@ -26,88 +26,273 @@ const (
FOO_X FOO = 17
)
var FOO_name = map[int32]string{
// Enum value maps for FOO.
var (
FOO_name = map[int32]string{
17: "X",
}
var FOO_value = map[string]int32{
}
FOO_value = map[string]int32{
"X": 17,
}
}
)
func (x FOO) Enum() *FOO {
p := new(FOO)
*p = x
return p
}
func (x FOO) String() string {
return proto.EnumName(FOO_name, int32(x))
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (x *FOO) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO")
func (FOO) Descriptor() protoreflect.EnumDescriptor {
return file_test_proto_enumTypes[0].Descriptor()
}
func (FOO) Type() protoreflect.EnumType {
return &file_test_proto_enumTypes[0]
}
func (x FOO) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Do not use.
func (x *FOO) UnmarshalJSON(b []byte) error {
num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
if err != nil {
return err
}
*x = FOO(value)
*x = FOO(num)
return nil
}
// Deprecated: Use FOO.Descriptor instead.
func (FOO) EnumDescriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
type Test struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
XXX_unrecognized []byte `json:"-"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup,json=optionalgroup" json:"optionalgroup,omitempty"`
}
// Default values for Test fields.
const (
Default_Test_Type = int32(77)
)
func (x *Test) Reset() {
*x = Test{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Test) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (m *Test) Reset() { *m = Test{} }
func (m *Test) String() string { return proto.CompactTextString(m) }
func (*Test) ProtoMessage() {}
const Default_Test_Type int32 = 77
func (x *Test) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
func (m *Test) GetLabel() string {
if m != nil && m.Label != nil {
return *m.Label
// Deprecated: Use Test.ProtoReflect.Descriptor instead.
func (*Test) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
func (x *Test) GetLabel() string {
if x != nil && x.Label != nil {
return *x.Label
}
return ""
}
func (m *Test) GetType() int32 {
if m != nil && m.Type != nil {
return *m.Type
func (x *Test) GetType() int32 {
if x != nil && x.Type != nil {
return *x.Type
}
return Default_Test_Type
}
func (m *Test) GetReps() []int64 {
if m != nil {
return m.Reps
func (x *Test) GetReps() []int64 {
if x != nil {
return x.Reps
}
return nil
}
func (m *Test) GetOptionalgroup() *Test_OptionalGroup {
if m != nil {
return m.Optionalgroup
func (x *Test) GetOptionalgroup() *Test_OptionalGroup {
if x != nil {
return x.Optionalgroup
}
return nil
}
type Test_OptionalGroup struct {
RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"`
XXX_unrecognized []byte `json:"-"`
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RequiredField *string `protobuf:"bytes,5,req,name=RequiredField" json:"RequiredField,omitempty"`
}
func (x *Test_OptionalGroup) Reset() {
*x = Test_OptionalGroup{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Test_OptionalGroup) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} }
func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) }
func (*Test_OptionalGroup) ProtoMessage() {}
func (m *Test_OptionalGroup) GetRequiredField() string {
if m != nil && m.RequiredField != nil {
return *m.RequiredField
func (x *Test_OptionalGroup) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Test_OptionalGroup.ProtoReflect.Descriptor instead.
func (*Test_OptionalGroup) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Test_OptionalGroup) GetRequiredField() string {
if x != nil && x.RequiredField != nil {
return *x.RequiredField
}
return ""
}
func init() {
proto.RegisterEnum("protoexample.FOO", FOO_name, FOO_value)
var File_test_proto protoreflect.FileDescriptor
var file_test_proto_rawDesc = []byte{
0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x04, 0x54,
0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x02,
0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x3a, 0x02, 0x37, 0x37, 0x52, 0x04, 0x74, 0x79, 0x70,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52,
0x04, 0x72, 0x65, 0x70, 0x73, 0x12, 0x46, 0x0a, 0x0d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61,
0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0a, 0x32, 0x20, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x54, 0x65, 0x73, 0x74,
0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x1a, 0x35, 0x0a,
0x0d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x24,
0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18,
0x05, 0x20, 0x02, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46,
0x69, 0x65, 0x6c, 0x64, 0x2a, 0x0c, 0x0a, 0x03, 0x46, 0x4f, 0x4f, 0x12, 0x05, 0x0a, 0x01, 0x58,
0x10, 0x11,
}
var (
file_test_proto_rawDescOnce sync.Once
file_test_proto_rawDescData = file_test_proto_rawDesc
)
func file_test_proto_rawDescGZIP() []byte {
file_test_proto_rawDescOnce.Do(func() {
file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData)
})
return file_test_proto_rawDescData
}
var file_test_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_test_proto_goTypes = []interface{}{
(FOO)(0), // 0: protoexample.FOO
(*Test)(nil), // 1: protoexample.Test
(*Test_OptionalGroup)(nil), // 2: protoexample.Test.OptionalGroup
}
var file_test_proto_depIdxs = []int32{
2, // 0: protoexample.Test.optionalgroup:type_name -> protoexample.Test.OptionalGroup
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_test_proto_init() }
func file_test_proto_init() {
if File_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Test); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Test_OptionalGroup); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_test_proto_rawDesc,
NumEnums: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_test_proto_goTypes,
DependencyIndexes: file_test_proto_depIdxs,
EnumInfos: file_test_proto_enumTypes,
MessageInfos: file_test_proto_msgTypes,
}.Build()
File_test_proto = out.File
file_test_proto_rawDesc = nil
file_test_proto_goTypes = nil
file_test_proto_depIdxs = nil
}

263
tree.go
View File

@ -17,6 +17,7 @@ import (
var (
strColon = []byte(":")
strStar = []byte("*")
strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value.
@ -30,8 +31,8 @@ type Param struct {
// It is therefore safe to read values by the index.
type Params []Param
// Get returns the value of the first Param which key matches the given name.
// If no matching Param is found, an empty string is returned.
// Get returns the value of the first Param which key matches the given name and a boolean true.
// If no matching Param is found, an empty string is returned and a boolean false .
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
@ -80,6 +81,16 @@ func longestCommonPrefix(a, b string) int {
return i
}
// addChild will add a child node, keeping wildcards at the end
func (n *node) addChild(child *node) {
if n.wildChild && len(n.children) > 0 {
wildcardChild := n.children[len(n.children)-1]
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
} else {
n.children = append(n.children, child)
}
}
func countParams(path string) uint16 {
var n uint16
s := bytesconv.StringToBytes(path)
@ -88,11 +99,15 @@ func countParams(path string) uint16 {
return n
}
func countSections(path string) uint16 {
s := bytesconv.StringToBytes(path)
return uint16(bytes.Count(s, strSlash))
}
type nodeType uint8
const (
static nodeType = iota // default
root
root nodeType = iota + 1
param
catchAll
)
@ -103,7 +118,7 @@ type node struct {
wildChild bool
nType nodeType
priority uint32
children []*node
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}
@ -119,7 +134,6 @@ func (n *node) incrementChildPrio(pos int) int {
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// Build new index char string
@ -178,36 +192,9 @@ walk:
// Make new node a child of this node
if i < len(path) {
path = path[i:]
if n.wildChild {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
c := path[0]
// slash after param
// '/' after param
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
@ -226,21 +213,47 @@ walk:
}
// Otherwise insert it
if c != ':' && c != '*' {
if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
n.children = append(n.children, child)
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
n = n.children[len(n.children)-1]
n.priority++
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise and handle to current node
// Otherwise add handle to current node
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
@ -294,13 +307,6 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
// Check if this node has existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
panic("wildcard segment '" + wildcard +
"' conflicts with existing children in path '" + fullPath + "'")
}
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
@ -308,13 +314,13 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
path = path[i:]
}
n.wildChild = true
child := &node{
nType: param,
path: wildcard,
fullPath: fullPath,
}
n.children = []*node{child}
n.addChild(child)
n.wildChild = true
n = child
n.priority++
@ -327,7 +333,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n.addChild(child)
n = child
continue
}
@ -361,7 +367,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
fullPath: fullPath,
}
n.children = []*node{child}
n.addChild(child)
n.indices = string('/')
n = child
n.priority++
@ -393,41 +399,90 @@ type nodeValue struct {
fullPath string
}
type skippedNode struct {
path string
node *node
paramsCount int16
}
// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16
walk: // Outer loop for walking the tree
for {
prefix := n.path
if len(path) > len(prefix) {
if path[:len(prefix)] == prefix {
path = path[len(prefix):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
if !n.wildChild {
// Try all the non-wildcard children first by matching the indices
idxc := path[0]
for i, c := range []byte(n.indices) {
if c == idxc {
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
if n.wildChild {
index := len(*skippedNodes)
*skippedNodes = (*skippedNodes)[:index+1]
(*skippedNodes)[index] = skippedNode{
path: prefix + path,
node: &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
},
paramsCount: globalParamsCount,
}
}
n = n.children[i]
continue walk
}
}
if !n.wildChild {
// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
// the current node needs to roll back to last vaild skippedNode
if path != "/" {
for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
}
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
value.tsr = (path == "/" && n.handlers != nil)
value.tsr = path == "/" && n.handlers != nil
return
}
// Handle wildcard child
n = n.children[0]
// Handle wildcard child, which is always at the end of the array
n = n.children[len(n.children)-1]
globalParamsCount++
switch n.nType {
case param:
// fix truncate the parameter
// tree_test.go line: 204
// Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
@ -435,7 +490,7 @@ walk: // Outer loop for walking the tree
}
// Save param value
if params != nil {
if params != nil && cap(*params) > 0 {
if value.params == nil {
value.params = params
}
@ -463,7 +518,7 @@ walk: // Outer loop for walking the tree
}
// ... but we can't
value.tsr = (len(path) == end+1)
value.tsr = len(path) == end+1
return
}
@ -475,7 +530,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil)
value.tsr = n.path == "/" && n.handlers != nil
}
return
@ -511,6 +566,24 @@ walk: // Outer loop for walking the tree
}
if path == prefix {
// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
// the current node needs to roll back to last vaild skippedNode
if n.handlers == nil && path != "/" {
for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
// n = latestNode.children[len(latestNode.children)-1]
}
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil {
@ -542,9 +615,27 @@ walk: // Outer loop for walking the tree
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") ||
value.tsr = path == "/" ||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == prefix[:len(prefix)-1] && n.handlers != nil)
// roll back to last valid skippedNode
if !value.tsr && path != "/" {
for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
}
return
}
}
@ -559,8 +650,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
// Use a static sized buffer on the stack in the common case.
// If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize)
if l := len(path) + 1; l > stackBufSize {
buf = make([]byte, 0, l)
if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, length)
}
ciPath := n.findCaseInsensitivePathRec(
@ -600,7 +691,30 @@ walk: // Outer loop for walking the tree
path = path[npLen:]
ciPath = append(ciPath, n.path...)
if len(path) > 0 {
if len(path) == 0 {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handlers != nil {
return ciPath
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
for i, c := range []byte(n.indices) {
if c == '/' {
n = n.children[i]
if (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) {
return append(ciPath, '/')
}
return nil
}
}
}
return nil
}
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
@ -735,29 +849,6 @@ walk: // Outer loop for walking the tree
default:
panic("invalid node type")
}
} else {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handlers != nil {
return ciPath
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
for i, c := range []byte(n.indices) {
if c == '/' {
n = n.children[i]
if (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) {
return append(ciPath, '/')
}
return nil
}
}
}
return nil
}
}
// Nothing found.

View File

@ -33,6 +33,11 @@ func getParams() *Params {
return &ps
}
func getSkippedNodes() *[]skippedNode {
ps := make([]skippedNode, 0, 20)
return &ps
}
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
@ -40,7 +45,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
for _, request := range requests {
value := tree.getValue(request.path, getParams(), unescape)
value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
if value.handlers == nil {
if !request.nilHandler {
@ -135,11 +140,16 @@ func TestTreeWildcard(t *testing.T) {
routes := [...]string{
"/",
"/cmd/:tool/:sub",
"/cmd/:tool/",
"/cmd/:tool/:sub",
"/cmd/whoami",
"/cmd/whoami/root",
"/cmd/whoami/root/",
"/src/*filepath",
"/search/",
"/search/:query",
"/search/gin-gonic",
"/search/google",
"/user_:name",
"/user_:name/about",
"/files/:dir/*filepath",
@ -148,6 +158,40 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html",
"/info/:user/public",
"/info/:user/project/:project",
"/info/:user/project/golang",
"/aa/*xx",
"/ab/*xx",
"/:cc",
"/c1/:dd/e",
"/c1/:dd/e1",
"/:cc/cc",
"/:cc/:dd/ee",
"/:cc/:dd/:ee/ff",
"/:cc/:dd/:ee/:ff/gg",
"/:cc/:dd/:ee/:ff/:gg/hh",
"/get/test/abc/",
"/get/:param/abc/",
"/something/:paramname/thirdthing",
"/something/secondthing/test",
"/get/abc",
"/get/:param",
"/get/abc/123abc",
"/get/abc/:param",
"/get/abc/123abc/xxx8",
"/get/abc/123abc/:param",
"/get/abc/123abc/xxx8/1234",
"/get/abc/123abc/xxx8/:param",
"/get/abc/123abc/xxx8/1234/ffas",
"/get/abc/123abc/xxx8/1234/:param",
"/get/abc/123abc/xxx8/1234/kkdd/12c",
"/get/abc/123abc/xxx8/1234/kkdd/:param",
"/get/abc/:param/test",
"/get/abc/123abd/:param",
"/get/abc/123abddd/:param",
"/get/abc/123/:param",
"/get/abc/123abg/:param",
"/get/abc/123abf/:param",
"/get/abc/123abfff/:param",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
@ -155,19 +199,122 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/whoami", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/cmd/whoami", nil},
{"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
{"/search/google", false, "/search/google", nil},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
// * Error with argument being intercepted
// new PR handle (/all /all/cc /a/cc)
// fix PR: https://github.com/gin-gonic/gin/pull/2796
{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
{"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
{"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
{"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
{"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
{"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
{"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
{"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
{"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
{"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
{"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
{"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
{"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
{"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
{"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
{"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
{"/get/test/abc/", false, "/get/test/abc/", nil},
{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
{"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
{"/something/secondthing/test", false, "/something/secondthing/test", nil},
{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
{"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
{"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
{"/get/abc", false, "/get/abc", nil},
{"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
{"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
{"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
{"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
{"/get/abc/123abc", false, "/get/abc/123abc", nil},
{"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
{"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
{"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
{"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
{"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
{"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
{"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
{"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
{"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
{"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
{"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
{"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
{"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
{"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
{"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
{"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
{"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
{"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
{"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
{"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
{"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
{"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
{"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
})
checkPriorities(t, tree)
@ -245,20 +392,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{
{"/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/:file", true},
{"/src/static.json", true},
{"/src/*filepathx", true},
{"/src/", true},
{"/src/foo/bar", true},
{"/src1/", false},
{"/src1/*filepath", true},
{"/src2*filepath", true},
{"/src2/*filepath", false},
{"/search/:query", false},
{"/search/invalid", true},
{"/search/valid", false},
{"/user_:name", false},
{"/user_x", true},
{"/user_x", false},
{"/user_:name", 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)
}
@ -266,20 +431,23 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{
{"/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/*filepath", true},
{"/user_x", false},
{"/user_:name", true},
{"/user_:name", false},
{"/id/:id", false},
{"/id:id", true},
{"/:id", true},
{"/id:id", false},
{"/:id", false},
{"/*filepath", true},
}
testRoutes(t, routes)
}
func TestTreeDupliatePath(t *testing.T) {
func TestTreeDuplicatePath(t *testing.T) {
tree := &node{}
routes := [...]string{
@ -419,7 +587,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/go1.html",
"/no/a",
"/no/b",
"/api/hello/:name",
"/api/:page/:name",
"/api/hello/:name/bar/",
"/api/bar/:name",
"/api/baz/foo",
"/api/baz/foo/bar",
"/blog/:p",
"/posts/:b/:c",
"/posts/b/:c/d/",
}
for _, route := range routes {
recv := catchPanic(func() {
@ -445,9 +620,21 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/admin/config/",
"/admin/config/permissions/",
"/doc/",
"/admin/static/",
"/admin/cfg/",
"/admin/cfg/users/",
"/api/hello/x/bar",
"/api/baz/foo/",
"/api/baz/bax/",
"/api/bar/huh/",
"/api/baz/foo/bar/",
"/api/world/abc/",
"/blog/pp/",
"/posts/b/c/d",
}
for _, route := range tsrRoutes {
value := tree.getValue(route, nil, false)
value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !value.tsr {
@ -461,10 +648,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/no/",
"/_",
"/_/",
"/api/world/abc",
"/api",
"/api/",
"/api/hello/x/foo",
"/api/baz/foo/bad",
"/foo/p/p",
}
for _, route := range noTsrRoutes {
value := tree.getValue(route, nil, false)
value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if value.tsr {
@ -483,7 +674,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
value := tree.getValue("/", nil, false)
value := tree.getValue("/", nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler")
} else if value.tsr {
@ -663,7 +854,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup
recv := catchPanic(func() {
tree.getValue("/test", nil, false)
tree.getValue("/test", nil, getSkippedNodes(), false)
})
if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
@ -678,6 +869,19 @@ func TestTreeInvalidNodeType(t *testing.T) {
}
}
func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{}
tree.wildChild = true
tree.children = append(tree.children, &node{})
tree.children[0].nType = 2
// set invalid Params type
params := make(Params, 0)
// try to trigger slice bounds out of range with capacity 0
tree.getValue("/test", &params, getSkippedNodes(), false)
}
func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct {
route string
@ -688,8 +892,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/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
{"/con:nection", ":nection", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {

View File

@ -103,7 +103,10 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(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)
}
}

View File

@ -18,6 +18,12 @@ func init() {
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 {
T *testing.T
}

View File

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