Merge remote-tracking branch 'upstream/master' into github-actions

This commit is contained in:
laojianzi 2021-06-23 09:39:31 +08:00
commit d8b87a3682
31 changed files with 736 additions and 119 deletions

View File

@ -1,10 +1,49 @@
# Gin ChangeLog # Gin ChangeLog
## 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 ## 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 ### ENHANCEMENTS
* Support params and exact routes without creating conflicts [#2663](https://github.com/gin-gonic/gin/pull/2663) * 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

View File

@ -23,7 +23,8 @@ 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)
- [Build without `MsgPack` rendering feature](#build-without-msgpack-rendering-feature)
- [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 +85,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.12+ 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 +183,28 @@ 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 .
```
## 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 ## API Examples
@ -686,7 +702,7 @@ func main() {
// Example for binding XML ( // Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?> // <?xml version="1.0" encoding="UTF-8"?>
// <root> // <root>
// <user>user</user> // <user>manu</user>
// <password>123</password> // <password>123</password>
// </root>) // </root>)
router.POST("/loginXML", func(c *gin.Context) { router.POST("/loginXML", func(c *gin.Context) {
@ -925,7 +941,7 @@ func main() {
route.GET("/:name/:id", func(c *gin.Context) { route.GET("/:name/:id", func(c *gin.Context) {
var person Person var person Person
if err := c.ShouldBindUri(&person); err != nil { if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err}) c.JSON(400, gin.H{"msg": err.Error()})
return return
} }
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
@ -2124,6 +2140,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
@ -2182,3 +2231,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

@ -71,7 +71,7 @@ func (v *defaultValidator) validateStruct(obj interface{}) error {
// Engine returns the underlying validator engine which powers the default // Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations // Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info - // or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8 // https://pkg.go.dev/github.com/go-playground/validator/v10
func (v *defaultValidator) Engine() interface{} { func (v *defaultValidator) Engine() interface{} {
v.lazyinit() v.lazyinit()
return v.validate return v.validate

View File

@ -22,10 +22,8 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil && err != http.ErrNotMultipart {
if err != http.ErrNotMultipart { return err
return err
}
} }
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err

View File

@ -16,7 +16,15 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
var errUnknownType = errors.New("unknown type") var (
errUnknownType = errors.New("unknown type")
// ErrConvertMapStringSlice can not covert to map[string][]string
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
// ErrConvertToMapString can not convert to map[string]string
ErrConvertToMapString = errors.New("can not convert to map of strings")
)
func mapUri(ptr interface{}, m map[string][]string) error { func mapUri(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri") return mapFormByTag(ptr, m, "uri")
@ -109,7 +117,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if sf.PkgPath != "" && !sf.Anonymous { // unexported if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue continue
} }
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -371,7 +379,7 @@ func setFormMap(ptr interface{}, form map[string][]string) error {
if el.Kind() == reflect.Slice { if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string) ptrMap, ok := ptr.(map[string][]string)
if !ok { if !ok {
return errors.New("cannot convert to map slices of strings") return ErrConvertMapStringSlice
} }
for k, v := range form { for k, v := range form {
ptrMap[k] = v ptrMap[k] = v
@ -382,7 +390,7 @@ func setFormMap(ptr interface{}, form map[string][]string) error {
ptrMap, ok := ptr.(map[string]string) ptrMap, ok := ptr.(map[string]string)
if !ok { if !ok {
return errors.New("cannot convert to map of strings") return ErrConvertToMapString
} }
for k, v := range form { for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last ptrMap[k] = v[len(v)-1] // pick last

View File

@ -29,6 +29,6 @@ type headerSource map[string][]string
var _ setter = headerSource(nil) 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) return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
} }

View File

@ -6,7 +6,7 @@ package binding
import ( import (
"bytes" "bytes"
"fmt" "errors"
"io" "io"
"net/http" "net/http"
@ -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

@ -15,8 +15,16 @@ type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil) 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 // 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 { if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files) return setByMultipartFormFile(value, field, files)
} }
@ -49,12 +57,12 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file
case reflect.Array: case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files) 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) (isSetted bool, err error) {
if value.Len() != len(files) { if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader") return false, ErrMultiFileHeaderLenInvalid
} }
for i := range files { for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])

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) { func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename) 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() fl, err := fh.Open()
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -86,17 +86,17 @@ type Context struct {
func (c *Context) reset() { func (c *Context) reset() {
c.Writer = &c.writermem c.Writer = &c.writermem
c.Params = c.Params[0:0] c.Params = c.Params[:0]
c.handlers = nil c.handlers = nil
c.index = -1 c.index = -1
c.fullPath = "" c.fullPath = ""
c.Keys = nil c.Keys = nil
c.Errors = c.Errors[0:0] c.Errors = c.Errors[:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
c.formCache = nil c.formCache = nil
*c.params = (*c.params)[0:0] *c.params = (*c.params)[:0]
} }
// Copy returns a copy of the current context that can be safely used outside the request's scope. // Copy returns a copy of the current context that can be safely used outside the request's scope.
@ -725,32 +725,85 @@ 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 { switch {
clientIP := c.requestHeader("X-Forwarded-For") case c.engine.AppEngine:
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 addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr return addr
} }
case c.engine.CloudflareProxy:
if addr := c.requestHeader("CF-Connecting-IP"); 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.

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
@ -1391,12 +1395,11 @@ func TestContextAbortWithError(t *testing.T) {
func TestContextClientIP(t *testing.T) { func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") // Legacy tests (validating that the defaults don't break the
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") // (insecure!) old behaviour)
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, "20.20.20.20", c.ClientIP()) assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
@ -1416,6 +1419,82 @@ func TestContextClientIP(t *testing.T) {
// no port // no port
c.Request.RemoteAddr = "50.50.50.50" c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP()) assert.Empty(t, c.ClientIP())
// Tests exercising the TrustedProxies functionality
resetContextForClientIPTests(c)
// No trusted proxies
_ = c.engine.SetTrustedProxies([]string{})
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Last proxy is trusted, but the RemoteAddr is not
_ = c.engine.SetTrustedProxies([]string{"30.30.30.30"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// All steps are trusted
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30", "20.20.20.20"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use CIDR
_ = c.engine.SetTrustedProxies([]string{"40.40.25.25/16", "30.30.30.30"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use hostname that resolves to all the proxies
_ = c.engine.SetTrustedProxies([]string{"foo"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
_ = c.engine.SetTrustedProxies([]string{"bar"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", " blah ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Result from LookupHost has non-IP element. This should never
// happen, but we should test it to make sure we handle it
// gracefully.
_ = c.engine.SetTrustedProxies([]string{"baz"})
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Del("X-Forwarded-For")
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.engine.RemoteIPHeaders = []string{}
c.engine.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())
c.engine.AppEngine = false
c.engine.CloudflareProxy = true
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())
// 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.AppEngine = false
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -1960,3 +2039,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 = 12 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.12+. 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) { func debugPrintError(err error) {
if err != nil { if err != nil && IsDebugging() {
if IsDebugging() { fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
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()) 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.12+.\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,34 +0,0 @@
//go:build go1.13
// +build go1.13
package gin
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13,
// hence the "// +build go1.13" directive at the beginning of this file.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("somme error")
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
err := fmt.Errorf("wrapped: %w", &Error{
Err: innerErr,
Type: ErrorTypeAny,
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
}

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

2
fs.go
View File

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

111
gin.go
View File

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"strings"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
@ -81,12 +82,33 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound // If no other Method is allowed, the request is delegated to the NotFound
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with // If enabled, client IP will be parsed from the request's headers that
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
// fetched, it falls back to the IP obtained from
// `(*gin.Context).Request.RemoteAddr`.
ForwardedByClientIP bool
// List of headers used to obtain the client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of `(*gin.Engine).TrustedProxies`.
RemoteIPHeaders []string
// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
// IPv6 CIDRs) from which to trust request's headers that contain
// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
// `true`.
TrustedProxies []string
// #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS. // 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool AppEngine bool
// If enabled, it will trust the CF-Connecting-IP header to determine the
// IP of the client.
CloudflareProxy bool
// If enabled, the url.RawPath will be used to find parameters. // If enabled, the url.RawPath will be used to find parameters.
UseRawPath bool UseRawPath bool
@ -114,6 +136,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{}
@ -139,6 +162,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,
@ -305,12 +330,73 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
address := resolveAddress(addr) address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine) err = http.ListenAndServe(address, engine)
return return
} }
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.TrustedProxies == nil {
return nil, nil
}
cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
for _, trustedProxy := range engine.TrustedProxies {
if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy)
if ip == nil {
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
}
switch len(ip) {
case net.IPv4len:
trustedProxy += "/32"
case net.IPv6len:
trustedProxy += "/128"
}
}
_, cidrNet, err := net.ParseCIDR(trustedProxy)
if err != nil {
return cidr, err
}
cidr = append(cidr, cidrNet)
}
return cidr, nil
}
// SetTrustedProxies set Engine.TrustedProxies
func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
engine.TrustedProxies = trustedProxies
return engine.parseTrustedProxies()
}
// parseTrustedProxies parse Engine.TrustedProxies to Engine.trustedCIDRs
func (engine *Engine) parseTrustedProxies() error {
trustedCIDRs, err := engine.prepareTrustedCIDRs()
engine.trustedCIDRs = trustedCIDRs
return err
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
parsedIP := net.ParseIP(ip)
if ipv4 := parsedIP.To4(); ipv4 != nil {
// return ip in a 4-byte representation
return ipv4
}
// return ip in a 16-byte representation or nil
return parsedIP
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// 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.
@ -318,6 +404,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr) debugPrint("Listening and serving HTTPS on %s\n", addr)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine) err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
return return
} }
@ -329,6 +420,11 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file) debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
listener, err := net.Listen("unix", file) listener, err := net.Listen("unix", file)
if err != nil { if err != nil {
return return
@ -347,6 +443,11 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd) debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f) listener, err := net.FileListener(f)
if err != nil { if err != nil {
@ -362,6 +463,12 @@ func (engine *Engine) RunFd(fd int) (err error) {
func (engine *Engine) RunListener(listener net.Listener) (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()) debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }

View File

@ -55,6 +55,74 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
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) { func TestRunTLS(t *testing.T) {
router := New() router := New()
go func() { go func() {

View File

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

5
go.mod
View File

@ -4,11 +4,12 @@ go 1.13
require ( require (
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.4.1 github.com/go-playground/validator/v10 v10.6.1
github.com/goccy/go-json v0.5.1
github.com/golang/protobuf v1.3.3 github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7 github.com/ugorji/go/codec v1.2.6
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
) )

15
go.sum
View File

@ -9,8 +9,10 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/goccy/go-json v0.5.1 h1:R9UYTOUvo7eIY9aeDMZ4L6OVtHaSr1k2No9W6MKjXrA=
github.com/goccy/go-json v0.5.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3 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=
@ -30,10 +32,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -43,6 +45,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 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/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

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,8 +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.
//go:build !jsoniter //go:build !jsoniter && !go_json
// +build !jsoniter // +build !jsoniter,!go_json
package json package json

View File

@ -138,8 +138,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
} }
if param.Latency > time.Minute { if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way param.Latency = param.Latency.Truncate(time.Second)
param.Latency = param.Latency - param.Latency%time.Second
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s", return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),

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

@ -46,9 +46,11 @@ type PureJSON struct {
Data interface{} Data interface{}
} }
var jsonContentType = []string{"application/json; charset=utf-8"} var (
var jsonpContentType = []string{"application/javascript; charset=utf-8"} jsonContentType = []string{"application/json; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"} jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonAsciiContentType = []string{"application/json"}
)
// Render (JSON) writes data with custom ContentType. // Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {

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

@ -360,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"))

33
tree.go
View File

@ -118,6 +118,11 @@ type node struct {
fullPath string fullPath string
} }
type skip struct {
path string
paramNode *node
}
// Increments priority of the given child and reorders if necessary // Increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int { func (n *node) incrementChildPrio(pos int) int {
cs := n.children cs := n.children
@ -400,6 +405,8 @@ type nodeValue struct {
// made if a handle exists with an extra (without the) trailing slash for the // made if a handle exists with an extra (without the) trailing slash for the
// given path. // given path.
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) { func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
var skipped *skip
walk: // Outer loop for walking the tree walk: // Outer loop for walking the tree
for { for {
prefix := n.path prefix := n.path
@ -411,6 +418,21 @@ walk: // Outer loop for walking the tree
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 {
if strings.HasPrefix(n.children[len(n.children)-1].path, ":") {
skipped = &skip{
path: prefix + path,
paramNode: &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
},
}
}
n = n.children[i] n = n.children[i]
continue walk continue walk
} }
@ -465,7 +487,7 @@ walk: // Outer loop for walking the tree
} }
// ... but we can't // ... but we can't
value.tsr = (len(path) == end+1) value.tsr = len(path) == end+1
return return
} }
@ -477,7 +499,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation // trailing slash exists for TSR recommendation
n = n.children[0] n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) value.tsr = n.path == "/" && n.handlers != nil
} }
return return
@ -542,6 +564,13 @@ walk: // Outer loop for walking the tree
return return
} }
if path != "/" && skipped != nil && strings.HasSuffix(skipped.path, path) {
path = skipped.path
n = skipped.paramNode
skipped = nil
continue walk
}
// Nothing found. We can recommend to redirect to the same URL with an // Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path // extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") || value.tsr = (path == "/") ||

View File

@ -135,13 +135,16 @@ func TestTreeWildcard(t *testing.T) {
routes := [...]string{ routes := [...]string{
"/", "/",
"/cmd/:tool/:sub",
"/cmd/:tool/", "/cmd/:tool/",
"/cmd/:tool/:sub",
"/cmd/whoami", "/cmd/whoami",
"/cmd/whoami/root",
"/cmd/whoami/root/", "/cmd/whoami/root/",
"/src/*filepath", "/src/*filepath",
"/search/", "/search/",
"/search/:query", "/search/:query",
"/search/gin-gonic",
"/search/google",
"/user_:name", "/user_:name",
"/user_:name/about", "/user_:name/about",
"/files/:dir/*filepath", "/files/:dir/*filepath",
@ -150,6 +153,7 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html", "/doc/go1.html",
"/info/:user/public", "/info/:user/public",
"/info/:user/project/:project", "/info/:user/project/:project",
"/info/:user/project/golang",
} }
for _, route := range routes { for _, route := range routes {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
@ -159,21 +163,29 @@ func TestTreeWildcard(t *testing.T) {
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}}, {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/", false, "/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", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/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}, {"/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"}}},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}}, {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil}, {"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
{"/search/google", false, "/search/google", nil},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}}, {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}}, {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)

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.2"