diff --git a/CHANGELOG.md b/CHANGELOG.md index a56d4ff8..a28edc84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,49 @@ # 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 +### 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) +* 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 diff --git a/README.md b/README.md index a042c4ad..ef37fd20 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [Quick start](#quick-start) - [Benchmarks](#benchmarks) - [Gin v1. stable](#gin-v1-stable) - - [Build with jsoniter](#build-with-jsoniter) + - [Build with jsoniter/go-json](#build-with-json-replacement) + - [Build without `MsgPack` rendering feature](#build-without-msgpack-rendering-feature) - [API Examples](#api-examples) - [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) - [Parameters in path](#parameters-in-path) @@ -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. -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 $ 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] API frozen, new releases will not break your code. -## Build with [jsoniter](https://github.com/json-iterator/go) +## Build with json replacement -Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. +Gin uses `encoding/json` as default json package but you can change it by build from other tags. +[jsoniter](https://github.com/json-iterator/go) ```sh $ go build -tags=jsoniter . ``` +[go-json](https://github.com/goccy/go-json) +```sh +$ go build -tags=go_json . +``` + +## Build without `MsgPack` rendering feature + +Gin enables `MsgPack` rendering feature by default. But you can disable this feature by specifying `nomsgpack` build tag. + +```sh +$ go build -tags=nomsgpack . +``` + +This is useful to reduce the binary size of executable files. See the [detail information](https://github.com/gin-gonic/gin/pull/1852). ## API Examples @@ -686,7 +702,7 @@ func main() { // Example for binding XML ( // // - // user + // manu // 123 // ) router.POST("/loginXML", func(c *gin.Context) { @@ -925,7 +941,7 @@ func main() { route.GET("/:name/:id", func(c *gin.Context) { var person Person if err := c.ShouldBindUri(&person); err != nil { - c.JSON(400, gin.H{"msg": err}) + c.JSON(400, gin.H{"msg": err.Error()}) return } c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) @@ -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 @@ -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. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. * [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system. + diff --git a/binding/default_validator.go b/binding/default_validator.go index c57a120f..ee69329e 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -71,7 +71,7 @@ func (v *defaultValidator) validateStruct(obj interface{}) error { // Engine returns the underlying validator engine which powers the default // Validator instance. This is useful if you want to register custom validations // or struct level validations. See validator GoDoc for more info - -// https://godoc.org/gopkg.in/go-playground/validator.v8 +// https://pkg.go.dev/github.com/go-playground/validator/v10 func (v *defaultValidator) Engine() interface{} { v.lazyinit() return v.validate diff --git a/binding/form.go b/binding/form.go index b93c34cf..040af9e2 100644 --- a/binding/form.go +++ b/binding/form.go @@ -22,10 +22,8 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } - if err := req.ParseMultipartForm(defaultMemory); err != nil { - if err != http.ErrNotMultipart { - return err - } + if err := req.ParseMultipartForm(defaultMemory); err != nil && err != http.ErrNotMultipart { + return err } if err := mapForm(obj, req.Form); err != nil { return err diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 2f4e45b4..421c0f71 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -16,7 +16,15 @@ import ( "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 { 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 continue } - ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) + ok, err := mapping(value.Field(i), sf, setter, tag) if err != nil { return false, err } @@ -371,7 +379,7 @@ func setFormMap(ptr interface{}, form map[string][]string) error { if el.Kind() == reflect.Slice { ptrMap, ok := ptr.(map[string][]string) if !ok { - return errors.New("cannot convert to map slices of strings") + return ErrConvertMapStringSlice } for k, v := range form { ptrMap[k] = v @@ -382,7 +390,7 @@ func setFormMap(ptr interface{}, form map[string][]string) error { ptrMap, ok := ptr.(map[string]string) if !ok { - return errors.New("cannot convert to map of strings") + return ErrConvertToMapString } for k, v := range form { ptrMap[k] = v[len(v)-1] // pick last diff --git a/binding/header.go b/binding/header.go index 179ce4ea..b99302af 100644 --- a/binding/header.go +++ b/binding/header.go @@ -29,6 +29,6 @@ type headerSource map[string][]string var _ setter = headerSource(nil) -func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) { +func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) { return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt) } diff --git a/binding/json.go b/binding/json.go index d62e0705..45aaa494 100644 --- a/binding/json.go +++ b/binding/json.go @@ -6,7 +6,7 @@ package binding import ( "bytes" - "fmt" + "errors" "io" "net/http" @@ -32,7 +32,7 @@ func (jsonBinding) Name() string { func (jsonBinding) Bind(req *http.Request, obj interface{}) error { if req == nil || req.Body == nil { - return fmt.Errorf("invalid request") + return errors.New("invalid request") } return decodeJSON(req.Body, obj) } diff --git a/binding/multipart_form_mapping.go b/binding/multipart_form_mapping.go index f85a1aa6..69c0a544 100644 --- a/binding/multipart_form_mapping.go +++ b/binding/multipart_form_mapping.go @@ -15,8 +15,16 @@ type multipartRequest http.Request var _ setter = (*multipartRequest)(nil) +var ( + // ErrMultiFileHeader multipart.FileHeader invalid + ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader") + + // ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid + ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*multipart.FileHeader") +) + // TrySet tries to set a value by the multipart request with the binding a form file -func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { +func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (bool, error) { if files := r.MultipartForm.File[key]; len(files) != 0 { return setByMultipartFormFile(value, field, files) } @@ -49,12 +57,12 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file case reflect.Array: return setArrayOfMultipartFormFiles(value, field, files) } - return false, errors.New("unsupported field type for multipart.FileHeader") + return false, ErrMultiFileHeader } func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { if value.Len() != len(files) { - return false, errors.New("unsupported len of array for []*multipart.FileHeader") + return false, ErrMultiFileHeaderLenInvalid } for i := range files { setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) diff --git a/binding/multipart_form_mapping_test.go b/binding/multipart_form_mapping_test.go index 4c75d1fe..4aaa60be 100644 --- a/binding/multipart_form_mapping_test.go +++ b/binding/multipart_form_mapping_test.go @@ -124,7 +124,7 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) { assert.Equal(t, file.Filename, fh.Filename) - // assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8 + assert.Equal(t, int64(len(file.Content)), fh.Size) fl, err := fh.Open() assert.NoError(t, err) diff --git a/context.go b/context.go index 3d6b56d6..0c1fb07f 100644 --- a/context.go +++ b/context.go @@ -86,17 +86,17 @@ type Context struct { func (c *Context) reset() { c.Writer = &c.writermem - c.Params = c.Params[0:0] + c.Params = c.Params[:0] c.handlers = nil c.index = -1 c.fullPath = "" c.Keys = nil - c.Errors = c.Errors[0:0] + c.Errors = c.Errors[:0] c.Accepted = nil c.queryCache = nil c.formCache = nil - *c.params = (*c.params)[0:0] + *c.params = (*c.params)[:0] } // 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) } -// ClientIP implements a best effort algorithm to return the real client IP, it parses -// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. -// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. +// ClientIP implements a best effort algorithm to return the real client IP. +// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. +// If it'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 { - 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 { + switch { + case c.engine.AppEngine: if addr := c.requestHeader("X-Appengine-Remote-Addr"); 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 { - return ip + remoteIP, trusted := c.RemoteIP() + if remoteIP == nil { + return "" } - return "" + if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { + for _, headerName := range c.engine.RemoteIPHeaders { + ip, valid := 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. diff --git a/context_test.go b/context_test.go index 8e1e3b57..e0de717e 100644 --- a/context_test.go +++ b/context_test.go @@ -1018,7 +1018,9 @@ func TestContextRenderFile(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) + // Content-Type='text/plain; charset=utf-8' when go version <= 1.16, + // else, Content-Type='text/x-go; charset=utf-8' + assert.NotEqual(t, "", w.Header().Get("Content-Type")) } func TestContextRenderFileFromFS(t *testing.T) { @@ -1030,7 +1032,9 @@ func TestContextRenderFileFromFS(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) + // Content-Type='text/plain; charset=utf-8' when go version <= 1.16, + // else, Content-Type='text/x-go; charset=utf-8' + assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.Equal(t, "/some/path", c.Request.URL.Path) } @@ -1044,7 +1048,7 @@ func TestContextRenderAttachment(t *testing.T) { assert.Equal(t, 200, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition")) + assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition")) } // TestContextRenderYAML tests that the response is serialized as YAML @@ -1391,12 +1395,11 @@ func TestContextAbortWithError(t *testing.T) { func TestContextClientIP(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) + c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs() + resetContextForClientIPTests(c) - c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") - c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") - c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50") - c.Request.RemoteAddr = " 40.40.40.40:42123 " - + // Legacy tests (validating that the defaults don't break the + // (insecure!) old behaviour) assert.Equal(t, "20.20.20.20", c.ClientIP()) c.Request.Header.Del("X-Forwarded-For") @@ -1416,6 +1419,82 @@ func TestContextClientIP(t *testing.T) { // no port c.Request.RemoteAddr = "50.50.50.50" assert.Empty(t, c.ClientIP()) + + // Tests exercising the TrustedProxies functionality + resetContextForClientIPTests(c) + + // No trusted proxies + _ = c.engine.SetTrustedProxies([]string{}) + c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"} + assert.Equal(t, "40.40.40.40", c.ClientIP()) + + // 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) { @@ -1960,3 +2039,12 @@ func TestContextWithKeysMutex(t *testing.T) { assert.Nil(t, value) assert.False(t, err) } + +func TestRemoteIPFail(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.RemoteAddr = "[:::]:80" + ip, trust := c.RemoteIP() + assert.Nil(t, ip) + assert.False(t, trust) +} diff --git a/debug.go b/debug.go index 4c7cd0c3..ed313868 100644 --- a/debug.go +++ b/debug.go @@ -12,7 +12,7 @@ import ( "strings" ) -const ginSupportMinGoVer = 12 +const ginSupportMinGoVer = 13 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. @@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) { func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { - debugPrint(`[WARNING] Now Gin requires Go 1.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) { - if err != nil { - if IsDebugging() { - fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err) - } + if err != nil && IsDebugging() { + fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err) } } diff --git a/debug_test.go b/debug_test.go index c2272d0f..05509992 100644 --- a/debug_test.go +++ b/debug_test.go @@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) { }) m, e := getMinVer(runtime.Version()) if e == nil && m <= ginSupportMinGoVer { - assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.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 { assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } diff --git a/errors_1.13_test.go b/errors_1.13_test.go deleted file mode 100644 index 5fb6057b..00000000 --- a/errors_1.13_test.go +++ /dev/null @@ -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)) -} diff --git a/errors_test.go b/errors_test.go index 6aae1c10..ee95ab31 100644 --- a/errors_test.go +++ b/errors_test.go @@ -6,6 +6,7 @@ package gin import ( "errors" + "fmt" "testing" "github.com/gin-gonic/gin/internal/json" @@ -104,3 +105,24 @@ Error #03: third assert.Nil(t, errs.JSON()) assert.Empty(t, errs.String()) } + +type TestErr string + +func (e TestErr) Error() string { return string(e) } + +// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()". +// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13. +func TestErrorUnwrap(t *testing.T) { + innerErr := TestErr("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)) +} diff --git a/fs.go b/fs.go index 007d9b75..e5f3d602 100644 --- a/fs.go +++ b/fs.go @@ -17,7 +17,7 @@ type neuteredReaddirFile struct { http.File } -// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally +// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally // in router.Static(). // if listDirectory == true, then it works the same as http.Dir() otherwise it returns // a filesystem that prevents http.FileServer() to list the directory files. diff --git a/gin.go b/gin.go index 1e126179..00686e77 100644 --- a/gin.go +++ b/gin.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path" + "strings" "sync" "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 // handler. 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. 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. UseRawPath bool @@ -114,6 +136,7 @@ type Engine struct { pool sync.Pool trees methodTrees maxParams uint16 + trustedCIDRs []*net.IPNet } var _ IRouter = &Engine{} @@ -139,6 +162,8 @@ func New() *Engine { RedirectFixedPath: false, HandleMethodNotAllowed: false, ForwardedByClientIP: true, + RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, + TrustedProxies: []string{"0.0.0.0/0"}, AppEngine: defaultAppEngine, UseRawPath: 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) { defer func() { debugPrintError(err) }() + err = engine.parseTrustedProxies() + if err != nil { + return err + } + address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return } +func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) { + if engine.TrustedProxies == nil { + return nil, nil + } + + cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies)) + for _, trustedProxy := range engine.TrustedProxies { + if !strings.Contains(trustedProxy, "/") { + ip := parseIP(trustedProxy) + if ip == nil { + return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy} + } + + switch len(ip) { + case net.IPv4len: + trustedProxy += "/32" + case net.IPv6len: + trustedProxy += "/128" + } + } + _, cidrNet, err := net.ParseCIDR(trustedProxy) + if err != nil { + return cidr, err + } + cidr = append(cidr, cidrNet) + } + return cidr, nil +} + +// SetTrustedProxies set 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. // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // 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) defer func() { debugPrintError(err) }() + err = engine.parseTrustedProxies() + if err != nil { + return err + } + err = http.ListenAndServeTLS(addr, certFile, keyFile, engine) return } @@ -329,6 +420,11 @@ func (engine *Engine) RunUnix(file string) (err error) { debugPrint("Listening and serving HTTP on unix:/%s", file) defer func() { debugPrintError(err) }() + err = engine.parseTrustedProxies() + if err != nil { + return err + } + listener, err := net.Listen("unix", file) if err != nil { return @@ -347,6 +443,11 @@ func (engine *Engine) RunFd(fd int) (err error) { debugPrint("Listening and serving HTTP on fd@%d", fd) defer func() { debugPrintError(err) }() + err = engine.parseTrustedProxies() + if err != nil { + return err + } + f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) listener, err := net.FileListener(f) if err != nil { @@ -362,6 +463,12 @@ func (engine *Engine) RunFd(fd int) (err error) { func (engine *Engine) RunListener(listener net.Listener) (err error) { debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr()) defer func() { debugPrintError(err) }() + + err = engine.parseTrustedProxies() + if err != nil { + return err + } + err = http.Serve(listener, engine) return } diff --git a/gin_integration_test.go b/gin_integration_test.go index 41ad9874..2eb2d52b 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -55,6 +55,74 @@ func TestRunEmpty(t *testing.T) { 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) { router := New() go func() { diff --git a/gin_test.go b/gin_test.go index 11bdd79c..678d95f2 100644 --- a/gin_test.go +++ b/gin_test.go @@ -9,6 +9,7 @@ import ( "fmt" "html/template" "io/ioutil" + "net" "net/http" "net/http/httptest" "reflect" @@ -532,6 +533,139 @@ func TestEngineHandleContextManyReEntries(t *testing.T) { assert.Equal(t, int64(expectValue), middlewareCounter) } +func TestPrepareTrustedCIRDsWith(t *testing.T) { + r := New() + + // valid ipv4 cidr + { + expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")} + 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) { for _, gotRoute := range gotRoutes { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { diff --git a/go.mod b/go.mod index 884ff851..9484b264 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.13 require ( 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/json-iterator/go v1.1.9 github.com/mattn/go-isatty v0.0.12 github.com/stretchr/testify v1.4.0 - github.com/ugorji/go/codec v1.1.7 + github.com/ugorji/go/codec v1.2.6 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index a64b3319..e61ef908 100644 --- a/go.sum +++ b/go.sum @@ -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/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.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.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= +github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/goccy/go-json v0.5.1 h1:R9UYTOUvo7eIY9aeDMZ4L6OVtHaSr1k2No9W6MKjXrA= +github.com/goccy/go-json v0.5.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/json/go_json.go b/internal/json/go_json.go new file mode 100644 index 00000000..da960571 --- /dev/null +++ b/internal/json/go_json.go @@ -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 +) diff --git a/internal/json/json.go b/internal/json/json.go index 172aeb24..75b60224 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -//go:build !jsoniter -// +build !jsoniter +//go:build !jsoniter && !go_json +// +build !jsoniter,!go_json package json diff --git a/logger.go b/logger.go index d361b74d..22138a8d 100644 --- a/logger.go +++ b/logger.go @@ -138,8 +138,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string { } if param.Latency > time.Minute { - // Truncate in a golang < 1.8 safe way - param.Latency = param.Latency - param.Latency%time.Second + param.Latency = param.Latency.Truncate(time.Second) } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s", param.TimeStamp.Format("2006/01/02 - 15:04:05"), diff --git a/logger_test.go b/logger_test.go index 0d40666e..80961ce1 100644 --- a/logger_test.go +++ b/logger_test.go @@ -185,6 +185,8 @@ func TestLoggerWithConfigFormatting(t *testing.T) { buffer := new(bytes.Buffer) router := New() + router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs() + router.Use(LoggerWithConfig(LoggerConfig{ Output: buffer, Formatter: func(param LogFormatterParams) string { diff --git a/render/json.go b/render/json.go index 41863093..e25415b0 100644 --- a/render/json.go +++ b/render/json.go @@ -46,9 +46,11 @@ type PureJSON struct { Data interface{} } -var jsonContentType = []string{"application/json; charset=utf-8"} -var jsonpContentType = []string{"application/javascript; charset=utf-8"} -var jsonAsciiContentType = []string{"application/json"} +var ( + jsonContentType = []string{"application/json; charset=utf-8"} + jsonpContentType = []string{"application/javascript; charset=utf-8"} + jsonAsciiContentType = []string{"application/json"} +) // Render (JSON) writes data with custom ContentType. func (r JSON) Render(w http.ResponseWriter) (err error) { diff --git a/routergroup.go b/routergroup.go index 15d9930d..6f14bf59 100644 --- a/routergroup.go +++ b/routergroup.go @@ -11,6 +11,11 @@ import ( "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. type IRouter interface { 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 // communication with a proxy). func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { - if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { + if matched := regEnLetter.MatchString(httpMethod); !matched { panic("http method " + httpMethod + " is not valid") } return group.handle(httpMethod, relativePath, handlers) diff --git a/routes_test.go b/routes_test.go index 485f0eea..036fa1c3 100644 --- a/routes_test.go +++ b/routes_test.go @@ -360,7 +360,9 @@ func TestRouterMiddlewareAndStatic(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "package gin") - assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) + // Content-Type='text/plain; charset=utf-8' when go version <= 1.16, + // else, Content-Type='text/x-go; charset=utf-8' + assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) diff --git a/tree.go b/tree.go index ca753e6d..051c74aa 100644 --- a/tree.go +++ b/tree.go @@ -118,6 +118,11 @@ type node struct { fullPath string } +type skip struct { + path string + paramNode *node +} + // Increments priority of the given child and reorders if necessary func (n *node) incrementChildPrio(pos int) int { cs := n.children @@ -400,6 +405,8 @@ type nodeValue struct { // made if a handle exists with an extra (without the) trailing slash for the // given path. func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) { + var skipped *skip + walk: // Outer loop for walking the tree for { prefix := n.path @@ -411,6 +418,21 @@ walk: // Outer loop for walking the tree idxc := path[0] for i, c := range []byte(n.indices) { 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] continue walk } @@ -465,7 +487,7 @@ walk: // Outer loop for walking the tree } // ... but we can't - value.tsr = (len(path) == end+1) + value.tsr = len(path) == end+1 return } @@ -477,7 +499,7 @@ walk: // Outer loop for walking the tree // No handle found. Check if a handle for this path + a // trailing slash exists for TSR recommendation n = n.children[0] - value.tsr = (n.path == "/" && n.handlers != nil) + value.tsr = n.path == "/" && n.handlers != nil } return @@ -542,6 +564,13 @@ walk: // Outer loop for walking the tree 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 // extra trailing slash if a leaf exists for that path value.tsr = (path == "/") || diff --git a/tree_test.go b/tree_test.go index d7c4fb0b..298c5ed0 100644 --- a/tree_test.go +++ b/tree_test.go @@ -135,13 +135,16 @@ func TestTreeWildcard(t *testing.T) { routes := [...]string{ "/", - "/cmd/:tool/:sub", "/cmd/:tool/", + "/cmd/:tool/:sub", "/cmd/whoami", + "/cmd/whoami/root", "/cmd/whoami/root/", "/src/*filepath", "/search/", "/search/:query", + "/search/gin-gonic", + "/search/google", "/user_:name", "/user_:name/about", "/files/:dir/*filepath", @@ -150,6 +153,7 @@ func TestTreeWildcard(t *testing.T) { "/doc/go1.html", "/info/:user/public", "/info/:user/project/:project", + "/info/:user/project/golang", } for _, route := range routes { tree.addRoute(route, fakeHandler(route)) @@ -159,21 +163,29 @@ func TestTreeWildcard(t *testing.T) { {"/", false, "/", nil}, {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}}, + {"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}}, + {"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}}, {"/cmd/whoami", false, "/cmd/whoami", nil}, {"/cmd/whoami/", true, "/cmd/whoami", nil}, + {"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}}, + {"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}}, + {"/cmd/whoami/root", false, "/cmd/whoami/root", nil}, {"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil}, - {"/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/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/search/", false, "/search/", nil}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, + {"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}}, + {"/search/gin-gonic", false, "/search/gin-gonic", nil}, + {"/search/google", false, "/search/google", nil}, {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}}, {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}}, {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, + {"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}}, }) checkPriorities(t, tree) diff --git a/version.go b/version.go index 3e9687dc..a80ab69a 100644 --- a/version.go +++ b/version.go @@ -5,4 +5,4 @@ package gin // Version is the current gin framework's version. -const Version = "v1.6.3" +const Version = "v1.7.2"