Merge branch 'master' into master

This commit is contained in:
thinkerou 2021-04-28 21:58:50 +08:00 committed by GitHub
commit fb741f0d63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1124 additions and 349 deletions

View File

@ -3,10 +3,6 @@ language: go
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
- go: 1.13.x - go: 1.13.x
env: env:
@ -19,6 +15,10 @@ matrix:
- go: 1.15.x - go: 1.15.x
env: env:
- TESTTAGS=nomsgpack - TESTTAGS=nomsgpack
- go: 1.16.x
- go: 1.16.x
env:
- TESTTAGS=nomsgpack
- go: master - go: master
git: git:

View File

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

View File

@ -1,5 +1,44 @@
# Gin ChangeLog # Gin ChangeLog
## 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 ## Gin v1.6.3
### ENHANCEMENTS ### ENHANCEMENTS

View File

@ -23,7 +23,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Quick start](#quick-start) - [Quick start](#quick-start)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Gin v1. stable](#gin-v1-stable) - [Gin v1. stable](#gin-v1-stable)
- [Build with jsoniter](#build-with-jsoniter) - [Build with jsoniter/go-json](#build-with-json-replacement)
- [API Examples](#api-examples) - [API Examples](#api-examples)
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) - [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path) - [Parameters in path](#parameters-in-path)
@ -84,7 +84,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. 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 ```sh
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
@ -182,13 +182,18 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- [x] Battle tested. - [x] Battle tested.
- [x] API frozen, new releases will not break your code. - [x] API frozen, new releases will not break your code.
## Build with [jsoniter](https://github.com/json-iterator/go) ## Build with 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 ```sh
$ go build -tags=jsoniter . $ go build -tags=jsoniter .
``` ```
[go-json](https://github.com/goccy/go-json)
```sh
$ go build -tags=go_json .
```
## API Examples ## API Examples
@ -243,6 +248,13 @@ func main() {
c.FullPath() == "/user/:name/*action" // true c.FullPath() == "/user/:name/*action" // true
}) })
// This handler will add a new router for /user/groups.
// Exact routes are resolved before param routes, regardless of the order they were defined.
// Routes starting with /user/groups are never interpreted as /user/:name/... routes
router.GET("/user/groups", func(c *gin.Context) {
c.String(http.StatusOK, "The available groups are [...]", name)
})
router.Run(":8080") router.Run(":8080")
} }
``` ```
@ -1797,8 +1809,8 @@ func main() {
// Initializing the server in a goroutine so that // Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below // it won't block the graceful shutdown handling below
go func() { go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %s\n", err) log.Printf("listen: %s\n", err)
} }
}() }()
@ -1816,6 +1828,7 @@ func main() {
// the request it is currently handling // the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err) log.Fatal("Server forced to shutdown:", err)
} }
@ -2120,6 +2133,39 @@ func main() {
} }
``` ```
## Don't trust all proxies
Gin lets you specify which headers to hold the real client IP (if any),
as well as specifying which proxies (or direct clients) you trust to
specify one of these headers.
The `TrustedProxies` slice on your `gin.Engine` specifes network addresses or
network CIDRs from where clients which their request headers related to client
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs.
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.TrustedProxies = []string{"192.168.1.2"}
router.GET("/", func(c *gin.Context) {
// If the client is 192.168.1.2, use the X-Forwarded-For
// header to deduce the original client IP from the trust-
// worthy parts of that header.
// Otherwise, simply return the direct client IP
fmt.Printf("ClientIP: %s\n", c.ClientIP())
})
router.Run()
}
```
## Testing ## Testing
@ -2178,3 +2224,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. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system. * [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.

View File

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

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package binding package binding
@ -51,7 +52,8 @@ type BindingUri interface {
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned. // If the received type is a slice|array, the validation should be performed travel on every element.
// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed. // If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned. // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned. // Otherwise nil must be returned.

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"reflect"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -34,7 +35,7 @@ type QueryTest struct {
} }
type FooStruct struct { type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"` Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
} }
type FooBarStruct struct { type FooBarStruct struct {
@ -180,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`) `{"foo": "bar"}`, `{"bar": "foo"}`)
} }
func TestBindingJSONSlice(t *testing.T) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
}
func TestBindingJSONUseNumber(t *testing.T) { func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t, testBodyBindingUseNumber(t,
JSON, "json", JSON, "json",
@ -200,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`) `{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
} }
func TestBindingJSONStringMap(t *testing.T) {
testBodyBindingStringMap(t, JSON,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
}
func TestBindingForm(t *testing.T) { func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST", testFormBinding(t, "POST",
"/", "/", "/", "/",
@ -336,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer") "", "", "StructPointer")
} }
func TestBindingFormStringMap(t *testing.T) {
testBodyBindingStringMap(t, Form,
"/", "",
`foo=bar&hello=world`, "")
// Should pick the last value
testBodyBindingStringMap(t, Form,
"/", "",
`foo=something&foo=bar&hello=world`, "")
}
func TestBindingFormStringSliceMap(t *testing.T) {
obj := make(map[string][]string)
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err := Form.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
target := map[string][]string{
"foo": {"something", "bar"},
"hello": {"world"},
}
assert.True(t, reflect.DeepEqual(obj, target))
objInvalid := make(map[string][]int)
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err = Form.Bind(req, &objInvalid)
assert.Error(t, err)
}
func TestBindingQuery(t *testing.T) { func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST", testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/", "/?foo=bar&bar=foo", "/",
@ -366,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "") "bool_foo=unused", "")
} }
func TestBindingQueryStringMap(t *testing.T) {
b := Query
obj := make(map[string]string)
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
obj = make(map[string]string)
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
err = b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "2", obj["foo"])
assert.Equal(t, "world", obj["hello"])
}
func TestBindingXML(t *testing.T) { func TestBindingXML(t *testing.T) {
testBodyBinding(t, testBodyBinding(t,
XML, "xml", XML, "xml",
@ -387,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`) `foo: bar`, `bar: foo`)
} }
func TestBindingYAMLStringMap(t *testing.T) {
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
testBodyBindingStringMap(t, YAML,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
}
func TestBindingYAMLFail(t *testing.T) { func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t, testBodyBindingFail(t,
YAML, "yaml", YAML, "yaml",
@ -1114,6 +1195,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err) assert.Error(t, err)
} }
func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
var obj1 []FooStruct
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj1)
assert.NoError(t, err)
var obj2 []FooStruct
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj2)
assert.Error(t, err)
}
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string)
req := requestWithBody("POST", path, body)
if b.Name() == "form" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
if badPath != "" && badBody != "" {
obj = make(map[string]string)
req = requestWithBody("POST", badPath, badBody)
err = b.Bind(req, &obj)
assert.Error(t, err)
}
objInt := make(map[string]int)
req = requestWithBody("POST", path, body)
err = b.Bind(req, &objInt)
assert.Error(t, err)
}
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())

View File

@ -5,7 +5,9 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@ -16,24 +18,56 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type sliceValidateError []error
func (err sliceValidateError) Error() string {
var errMsgs []string
for i, e := range err {
if e == nil {
continue
}
errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
}
return strings.Join(errMsgs, "\n")
}
var _ StructValidator = &defaultValidator{} var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
value := reflect.ValueOf(obj) if obj == nil {
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
}
}
return 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 // Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations // Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info - // or struct level validations. See validator GoDoc for more info -

View File

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

View File

@ -29,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error {
var emptyField = reflect.StructField{} var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
// Check if ptr is a map
ptrVal := reflect.ValueOf(ptr)
var pointed interface{}
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface()
}
if ptrVal.Kind() == reflect.Map &&
ptrVal.Type().Key().Kind() == reflect.String {
if pointed != nil {
ptr = pointed
}
return setFormMap(ptr, form)
}
return mappingByPtr(ptr, formSource(form), tag) return mappingByPtr(ptr, formSource(form), tag)
} }
@ -349,3 +364,29 @@ func head(str, sep string) (head string, tail string) {
} }
return str[:idx], str[idx+len(sep):] return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr interface{}, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string)
if !ok {
return errors.New("cannot convert to map slices of strings")
}
for k, v := range form {
ptrMap[k] = v
}
return nil
}
ptrMap, ok := ptr.(map[string]string)
if !ok {
return errors.New("cannot convert to map of strings")
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

@ -725,32 +725,80 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj) return bb.BindBody(body, obj)
} }
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP.
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. // If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,
// the remote IP (coming form Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For")
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
}
if clientIP != "" {
return clientIP
}
}
if c.engine.AppEngine { if c.engine.AppEngine {
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr return addr
} }
} }
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { remoteIP, trusted := c.RemoteIP()
return ip if remoteIP == nil {
return ""
} }
return "" if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := validateHeader(c.requestHeader(headerName))
if valid {
return ip
}
}
}
return remoteIP.String()
}
// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined in Engine.TrustedProxies
func (c *Context) RemoteIP() (net.IP, bool) {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return nil, false
}
remoteIP := net.ParseIP(ip)
if remoteIP == nil {
return nil, false
}
if c.engine.trustedCIDRs != nil {
for _, cidr := range c.engine.trustedCIDRs {
if cidr.Contains(remoteIP) {
return remoteIP, true
}
}
}
return remoteIP, false
}
func validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i, ipStr := range items {
ipStr = strings.TrimSpace(ipStr)
ip := net.ParseIP(ipStr)
if ip == nil {
return "", false
}
// We need to return the first IP in the list, but,
// we should not early return since we need to validate that
// the rest of the header is syntactically valid
if i == 0 {
clientIP = ipStr
valid = true
}
}
return
} }
// ContentType returns the Content-Type header of the request. // ContentType returns the Content-Type header of the request.
@ -891,7 +939,7 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
} }
// JSONP serializes the given struct as JSON into the response body. // JSONP serializes the given struct as JSON into the response body.
// It add padding to response body to request data from a server residing in a different domain than the client. // It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript". // It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) { func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "") callback := c.DefaultQuery("callback", "")
@ -968,7 +1016,7 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
}) })
} }
// File writes the specified file into the body stream in a efficient way. // File writes the specified file into the body stream in an efficient way.
func (c *Context) File(filepath string) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }

View File

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

View File

@ -1018,7 +1018,9 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
} }
func TestContextRenderFileFromFS(t *testing.T) { func TestContextRenderFileFromFS(t *testing.T) {
@ -1030,7 +1032,9 @@ func TestContextRenderFileFromFS(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path) assert.Equal(t, "/some/path", c.Request.URL.Path)
} }
@ -1044,7 +1048,7 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition")) assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
} }
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
@ -1388,15 +1392,18 @@ func TestContextAbortWithError(t *testing.T) {
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
func resetTrustedCIDRs(c *Context) {
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
}
func TestContextClientIP(t *testing.T) { func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
resetTrustedCIDRs(c)
resetContextForClientIPTests(c)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") // Legacy tests (validating that the defaults don't break the
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") // (insecure!) old behaviour)
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, "20.20.20.20", c.ClientIP()) assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
@ -1416,6 +1423,84 @@ func TestContextClientIP(t *testing.T) {
// no port // no port
c.Request.RemoteAddr = "50.50.50.50" c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP()) assert.Empty(t, c.ClientIP())
// Tests exercising the TrustedProxies functionality
resetContextForClientIPTests(c)
// No trusted proxies
c.engine.TrustedProxies = []string{}
resetTrustedCIDRs(c)
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Last proxy is trusted, but the RemoteAddr is not
c.engine.TrustedProxies = []string{"30.30.30.30"}
resetTrustedCIDRs(c)
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
c.engine.TrustedProxies = []string{"40.40.40.40"}
resetTrustedCIDRs(c)
assert.Equal(t, "20.20.20.20", c.ClientIP())
// All steps are trusted
c.engine.TrustedProxies = []string{"40.40.40.40", "30.30.30.30", "20.20.20.20"}
resetTrustedCIDRs(c)
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use CIDR
c.engine.TrustedProxies = []string{"40.40.25.25/16", "30.30.30.30"}
resetTrustedCIDRs(c)
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use hostname that resolves to all the proxies
c.engine.TrustedProxies = []string{"foo"}
resetTrustedCIDRs(c)
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
c.engine.TrustedProxies = []string{"bar"}
resetTrustedCIDRs(c)
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
c.engine.TrustedProxies = []string{"40.40.40.40"}
resetTrustedCIDRs(c)
c.Request.Header.Set("X-Forwarded-For", " blah ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Result from LookupHost has non-IP element. This should never
// happen, but we should test it to make sure we handle it
// gracefully.
c.engine.TrustedProxies = []string{"baz"}
resetTrustedCIDRs(c)
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedProxies = []string{"40.40.40.40"}
resetTrustedCIDRs(c)
c.Request.Header.Del("X-Forwarded-For")
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.engine.RemoteIPHeaders = []string{}
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
}
func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.AppEngine = false
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -1960,3 +2045,12 @@ func TestContextWithKeysMutex(t *testing.T) {
assert.Nil(t, value) assert.Nil(t, value)
assert.False(t, err) assert.False(t, err)
} }
func TestRemoteIPFail(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.RemoteAddr = "[:::]:80"
ip, trust := c.RemoteIP()
assert.Nil(t, ip)
assert.False(t, trust)
}

View File

@ -12,7 +12,7 @@ import (
"strings" "strings"
) )
const ginSupportMinGoVer = 10 const ginSupportMinGoVer = 13
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.13+.
`) `)
} }

View File

@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.13+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }

View File

@ -1,33 +0,0 @@
// +build go1.13
package gin
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13,
// hence the "// +build go1.13" directive at the beginning of this file.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("somme error")
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
err := fmt.Errorf("wrapped: %w", &Error{
Err: innerErr,
Type: ErrorTypeAny,
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
}

View File

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

70
gin.go
View File

@ -84,9 +84,26 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound // If no other Method is allowed, the request is delegated to the NotFound
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
// 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 ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with // List of headers used to obtain the client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of `(*gin.Engine).TrustedProxies`.
RemoteIPHeaders []string
// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
// IPv6 CIDRs) from which to trust request's headers that contain
// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
// `true`.
TrustedProxies []string
// #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS. // 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool AppEngine bool
@ -117,6 +134,7 @@ type Engine struct {
pool sync.Pool pool sync.Pool
trees methodTrees trees methodTrees
maxParams uint16 maxParams uint16
trustedCIDRs []*net.IPNet
} }
var _ IRouter = &Engine{} var _ IRouter = &Engine{}
@ -142,6 +160,8 @@ func New() *Engine {
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine, AppEngine: defaultAppEngine,
UseRawPath: false, UseRawPath: false,
RemoveExtraSlash: false, RemoveExtraSlash: false,
@ -327,12 +347,60 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
trustedCIDRs, err := engine.prepareTrustedCIDRs()
if err != nil {
return err
}
engine.trustedCIDRs = trustedCIDRs
address := resolveAddress(addr) address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine) err = http.ListenAndServe(address, engine)
return return
} }
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.TrustedProxies == nil {
return nil, nil
}
cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
for _, trustedProxy := range engine.TrustedProxies {
if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy)
if ip == nil {
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
}
switch len(ip) {
case net.IPv4len:
trustedProxy += "/32"
case net.IPv6len:
trustedProxy += "/128"
}
}
_, cidrNet, err := net.ParseCIDR(trustedProxy)
if err != nil {
return cidr, err
}
cidr = append(cidr, cidrNet)
}
return cidr, nil
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
parsedIP := net.ParseIP(ip)
if ipv4 := parsedIP.To4(); ipv4 != nil {
// return ip in a 4-byte representation
return ipv4
}
// return ip in a 16-byte representation or nil
return parsedIP
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.

View File

@ -14,6 +14,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -54,6 +55,13 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
}
func TestRunTLS(t *testing.T) { func TestRunTLS(t *testing.T) {
router := New() router := New()
go func() { go func() {
@ -146,7 +154,7 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) { func TestUnixSocket(t *testing.T) {
router := New() router := New()
unixTestSocket := "/tmp/unix_unit_test" unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket) defer os.Remove(unixTestSocket)

View File

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

2
go.sum
View File

@ -11,6 +11,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-json v0.4.11 h1:92nyX606ZN/cUFwctfxwDWm8YWSA38Zlv9s7taFeLyo=
github.com/goccy/go-json v0.4.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

View File

@ -5,16 +5,17 @@
package bytesconv package bytesconv
import ( import (
"reflect"
"unsafe" "unsafe"
) )
// StringToBytes converts string to byte slice without a memory allocation. // StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) { func StringToBytes(s string) []byte {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) return *(*[]byte)(unsafe.Pointer(
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) &struct {
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len string
return b Cap int
}{s, len(s)},
))
} }
// BytesToString converts byte slice to string without a memory allocation. // 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 // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !jsoniter //go:build !jsoniter && !go_json
// +build !jsoniter,!go_json
package json package json

View File

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

View File

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

View File

@ -63,7 +63,7 @@ func SetMode(value string) {
case TestMode: case TestMode:
ginMode = testCode ginMode = testCode
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value + " (available mode: debug release test)")
} }
modeName = value modeName = value

View File

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

View File

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

View File

@ -11,6 +11,11 @@ import (
"strings" "strings"
) )
var (
// reg match english letters for http method name
regEnLetter = regexp.MustCompile("^[A-Z]+$")
)
// IRouter defines all router handle interface includes single and group router. // IRouter defines all router handle interface includes single and group router.
type IRouter interface { type IRouter interface {
IRoutes IRoutes
@ -87,7 +92,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// frequently used, non-standardized or custom methods (e.g. for internal // frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy). // communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid") panic("http method " + httpMethod + " is not valid")
} }
return group.handle(httpMethod, relativePath, handlers) return group.handle(httpMethod, relativePath, handlers)

View File

@ -238,7 +238,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))
@ -272,7 +271,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))
@ -362,7 +360,9 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))

154
tree.go
View File

@ -80,6 +80,16 @@ func longestCommonPrefix(a, b string) int {
return i 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 { func countParams(path string) uint16 {
var n uint16 var n uint16
s := bytesconv.StringToBytes(path) s := bytesconv.StringToBytes(path)
@ -103,7 +113,7 @@ type node struct {
wildChild bool wildChild bool
nType nodeType nType nodeType
priority uint32 priority uint32
children []*node children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain handlers HandlersChain
fullPath string fullPath string
} }
@ -119,7 +129,6 @@ func (n *node) incrementChildPrio(pos int) int {
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions // Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
} }
// Build new index char string // Build new index char string
@ -178,36 +187,9 @@ walk:
// Make new node a child of this node // Make new node a child of this node
if i < len(path) { if i < len(path) {
path = path[i:] 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] c := path[0]
// slash after param // '/' after param
if n.nType == param && c == '/' && len(n.children) == 1 { if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
n = n.children[0] n = n.children[0]
@ -226,21 +208,47 @@ walk:
} }
// Otherwise insert it // Otherwise insert it
if c != ':' && c != '*' { if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65 // []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c}) n.indices += bytesconv.BytesToString([]byte{c})
child := &node{ child := &node{
fullPath: fullPath, fullPath: fullPath,
} }
n.children = append(n.children, child) n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1) n.incrementChildPrio(len(n.indices) - 1)
n = child 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) n.insertChild(path, fullPath, handlers)
return return
} }
// Otherwise and handle to current node // Otherwise add handle to current node
if n.handlers != nil { if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'") panic("handlers are already registered for path '" + fullPath + "'")
} }
@ -294,13 +302,6 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 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 wildcard[0] == ':' { // param
if i > 0 { if i > 0 {
// Insert prefix before the current wildcard // Insert prefix before the current wildcard
@ -308,13 +309,13 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
path = path[i:] path = path[i:]
} }
n.wildChild = true
child := &node{ child := &node{
nType: param, nType: param,
path: wildcard, path: wildcard,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.addChild(child)
n.wildChild = true
n = child n = child
n.priority++ n.priority++
@ -327,7 +328,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
priority: 1, priority: 1,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.addChild(child)
n = child n = child
continue continue
} }
@ -361,7 +362,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.addChild(child)
n.indices = string('/') n.indices = string('/')
n = child n = child
n.priority++ n.priority++
@ -405,10 +406,8 @@ walk: // Outer loop for walking the tree
if len(path) > len(prefix) { if len(path) > len(prefix) {
if path[:len(prefix)] == prefix { if path[:len(prefix)] == prefix {
path = path[len(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 // Try all the non-wildcard children first by matching the indices
// to walk down the tree
if !n.wildChild {
idxc := path[0] idxc := path[0]
for i, c := range []byte(n.indices) { for i, c := range []byte(n.indices) {
if c == idxc { if c == idxc {
@ -417,6 +416,8 @@ walk: // Outer loop for walking the tree
} }
} }
// If there is no wildcard pattern, recommend a redirection
if !n.wildChild {
// Nothing found. // Nothing found.
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
@ -424,8 +425,9 @@ walk: // Outer loop for walking the tree
return return
} }
// Handle wildcard child // Handle wildcard child, which is always at the end of the array
n = n.children[0] n = n.children[len(n.children)-1]
switch n.nType { switch n.nType {
case param: case param:
// Find param end (either '/' or path end) // Find param end (either '/' or path end)
@ -559,8 +561,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
// Use a static sized buffer on the stack in the common case. // 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. // If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize) buf := make([]byte, 0, stackBufSize)
if l := len(path) + 1; l > stackBufSize { if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, l) buf = make([]byte, 0, length)
} }
ciPath := n.findCaseInsensitivePathRec( ciPath := n.findCaseInsensitivePathRec(
@ -600,7 +602,30 @@ walk: // Outer loop for walking the tree
path = path[npLen:] path = path[npLen:]
ciPath = append(ciPath, n.path...) 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, // 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 // we can just look up the next child node and continue to walk down
// the tree // the tree
@ -735,29 +760,6 @@ walk: // Outer loop for walking the tree
default: default:
panic("invalid node type") 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. // Nothing found.

View File

@ -137,6 +137,8 @@ func TestTreeWildcard(t *testing.T) {
"/", "/",
"/cmd/:tool/:sub", "/cmd/:tool/:sub",
"/cmd/:tool/", "/cmd/:tool/",
"/cmd/whoami",
"/cmd/whoami/root/",
"/src/*filepath", "/src/*filepath",
"/search/", "/search/",
"/search/:query", "/search/:query",
@ -155,8 +157,12 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/whoami", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/cmd/whoami", nil},
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/cmd/whoami/root", true, "/cmd/whoami/root/", nil},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}}, {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}}, {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
@ -245,20 +251,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) { func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{ routes := []testRoute{
{"/cmd/:tool/:sub", false}, {"/cmd/:tool/:sub", false},
{"/cmd/vet", true}, {"/cmd/vet", false},
{"/foo/bar", false},
{"/foo/:name", false},
{"/foo/:names", true},
{"/cmd/*path", true},
{"/cmd/:badvar", true},
{"/cmd/:tool/names", false},
{"/cmd/:tool/:badsub/details", true},
{"/src/*filepath", false}, {"/src/*filepath", false},
{"/src/:file", true},
{"/src/static.json", true},
{"/src/*filepathx", true}, {"/src/*filepathx", true},
{"/src/", true}, {"/src/", true},
{"/src/foo/bar", true},
{"/src1/", false}, {"/src1/", false},
{"/src1/*filepath", true}, {"/src1/*filepath", true},
{"/src2*filepath", true}, {"/src2*filepath", true},
{"/src2/*filepath", false},
{"/search/:query", false}, {"/search/:query", false},
{"/search/invalid", true}, {"/search/valid", false},
{"/user_:name", false}, {"/user_:name", false},
{"/user_x", true}, {"/user_x", false},
{"/user_:name", false}, {"/user_:name", false},
{"/id:id", false}, {"/id:id", false},
{"/id/:id", true}, {"/id/:id", false},
}
testRoutes(t, routes)
}
func TestCatchAllAfterSlash(t *testing.T) {
routes := []testRoute{
{"/non-leading-*catchall", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -266,14 +290,17 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) { func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{ routes := []testRoute{
{"/cmd/vet", false}, {"/cmd/vet", false},
{"/cmd/:tool/:sub", true}, {"/cmd/:tool", false},
{"/cmd/:tool/:sub", false},
{"/cmd/:tool/misc", false},
{"/cmd/:tool/:othersub", true},
{"/src/AUTHORS", false}, {"/src/AUTHORS", false},
{"/src/*filepath", true}, {"/src/*filepath", true},
{"/user_x", false}, {"/user_x", false},
{"/user_:name", true}, {"/user_:name", false},
{"/id/:id", false}, {"/id/:id", false},
{"/id:id", true}, {"/id:id", false},
{"/:id", true}, {"/:id", false},
{"/*filepath", true}, {"/*filepath", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
@ -688,8 +715,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`}, {"/con:nection", ":nection", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
} }
for _, conflict := range conflicts { for _, conflict := range conflicts {

View File

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