mirror of https://github.com/gin-gonic/gin.git
Merge branch 'master' into fix-cors
This commit is contained in:
commit
5f94d6f3e8
|
@ -21,7 +21,7 @@ jobs:
|
||||||
- name: Setup golangci-lint
|
- name: Setup golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.42.1
|
version: v1.43.0
|
||||||
args: --verbose
|
args: --verbose
|
||||||
test:
|
test:
|
||||||
needs: lint
|
needs: lint
|
||||||
|
|
|
@ -109,6 +109,11 @@ People and companies, who have contributed, in alphabetical order.
|
||||||
- Fix typo in comment
|
- Fix typo in comment
|
||||||
|
|
||||||
|
|
||||||
|
**@jincheng9 (Jincheng Zhang)**
|
||||||
|
- ★ support TSR when wildcard follows named param
|
||||||
|
- Fix errors and typos in comments
|
||||||
|
|
||||||
|
|
||||||
**@joiggama (Ignacio Galindo)**
|
**@joiggama (Ignacio Galindo)**
|
||||||
- Add utf-8 charset header on renders
|
- Add utf-8 charset header on renders
|
||||||
|
|
||||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -1,5 +1,35 @@
|
||||||
# Gin ChangeLog
|
# Gin ChangeLog
|
||||||
|
|
||||||
|
## Gin v1.7.7
|
||||||
|
|
||||||
|
### BUGFIXES
|
||||||
|
|
||||||
|
* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
|
||||||
|
* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
|
||||||
|
* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
|
||||||
|
* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
|
||||||
|
* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
|
||||||
|
|
||||||
|
## Gin v1.7.6
|
||||||
|
|
||||||
|
### BUGFIXES
|
||||||
|
|
||||||
|
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
|
||||||
|
|
||||||
|
## Gin v1.7.4
|
||||||
|
|
||||||
|
### BUGFIXES
|
||||||
|
|
||||||
|
* bump new release to fix checksum mismatch
|
||||||
|
|
||||||
## Gin v1.7.3
|
## Gin v1.7.3
|
||||||
|
|
||||||
### BUGFIXES
|
### BUGFIXES
|
||||||
|
|
10
README.md
10
README.md
|
@ -78,7 +78,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
||||||
- [http2 server push](#http2-server-push)
|
- [http2 server push](#http2-server-push)
|
||||||
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
||||||
- [Set and get a cookie](#set-and-get-a-cookie)
|
- [Set and get a cookie](#set-and-get-a-cookie)
|
||||||
- [Don't trust all proxies](#don't-trust-all-proxies)
|
- [Don't trust all proxies](#dont-trust-all-proxies)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Users](#users)
|
- [Users](#users)
|
||||||
|
|
||||||
|
@ -384,8 +384,8 @@ func main() {
|
||||||
// Set a lower memory limit for multipart forms (default is 32 MiB)
|
// Set a lower memory limit for multipart forms (default is 32 MiB)
|
||||||
router.MaxMultipartMemory = 8 << 20 // 8 MiB
|
router.MaxMultipartMemory = 8 << 20 // 8 MiB
|
||||||
router.POST("/upload", func(c *gin.Context) {
|
router.POST("/upload", func(c *gin.Context) {
|
||||||
// single file
|
// Single file
|
||||||
file, _ := c.FormFile("file")
|
file, _ := c.FormFile("Filename")
|
||||||
log.Println(file.Filename)
|
log.Println(file.Filename)
|
||||||
|
|
||||||
// Upload the file to specific dst.
|
// Upload the file to specific dst.
|
||||||
|
@ -417,7 +417,7 @@ func main() {
|
||||||
router.POST("/upload", func(c *gin.Context) {
|
router.POST("/upload", func(c *gin.Context) {
|
||||||
// Multipart form
|
// Multipart form
|
||||||
form, _ := c.MultipartForm()
|
form, _ := c.MultipartForm()
|
||||||
files := form.File["upload[]"]
|
files := form.File["Filename[]"]
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
log.Println(file.Filename)
|
log.Println(file.Filename)
|
||||||
|
@ -906,7 +906,7 @@ func startPage(c *gin.Context) {
|
||||||
var person Person
|
var person Person
|
||||||
// If `GET`, only `Form` binding engine (`query`) used.
|
// If `GET`, only `Form` binding engine (`query`) used.
|
||||||
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
||||||
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L88
|
||||||
if c.ShouldBind(&person) == nil {
|
if c.ShouldBind(&person) == nil {
|
||||||
log.Println(person.Name)
|
log.Println(person.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Address)
|
||||||
|
|
|
@ -40,7 +40,7 @@ type BindingBody interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||||
// but it read the Params.
|
// but it reads the Params.
|
||||||
type BindingUri interface {
|
type BindingUri interface {
|
||||||
Name() string
|
Name() string
|
||||||
BindUri(map[string][]string, interface{}) error
|
BindUri(map[string][]string, interface{}) error
|
||||||
|
|
|
@ -38,7 +38,7 @@ type BindingBody interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||||
// but it read the Params.
|
// but it reads the Params.
|
||||||
type BindingUri interface {
|
type BindingUri interface {
|
||||||
Name() string
|
Name() string
|
||||||
BindUri(map[string][]string, interface{}) error
|
BindUri(map[string][]string, interface{}) error
|
||||||
|
|
|
@ -18,10 +18,10 @@ type defaultValidator struct {
|
||||||
validate *validator.Validate
|
validate *validator.Validate
|
||||||
}
|
}
|
||||||
|
|
||||||
type sliceValidateError []error
|
type SliceValidationError []error
|
||||||
|
|
||||||
// Error concatenates all error elements in sliceValidateError into a single string separated by \n.
|
// Error concatenates all error elements in SliceValidationError into a single string separated by \n.
|
||||||
func (err sliceValidateError) Error() string {
|
func (err SliceValidationError) Error() string {
|
||||||
n := len(err)
|
n := len(err)
|
||||||
switch n {
|
switch n {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -59,7 +59,7 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
||||||
return v.validateStruct(obj)
|
return v.validateStruct(obj)
|
||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
count := value.Len()
|
count := value.Len()
|
||||||
validateRet := make(sliceValidateError, 0)
|
validateRet := make(SliceValidationError, 0)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||||
validateRet = append(validateRet, err)
|
validateRet = append(validateRet, err)
|
||||||
|
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkSliceValidateError(b *testing.B) {
|
func BenchmarkSliceValidationError(b *testing.B) {
|
||||||
const size int = 100
|
const size int = 100
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
e := make(sliceValidateError, size)
|
e := make(SliceValidationError, size)
|
||||||
for j := 0; j < size; j++ {
|
for j := 0; j < size; j++ {
|
||||||
e[j] = errors.New(strconv.Itoa(j))
|
e[j] = errors.New(strconv.Itoa(j))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,24 +9,24 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSliceValidateError(t *testing.T) {
|
func TestSliceValidationError(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
err sliceValidateError
|
err SliceValidationError
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
|
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
|
||||||
{"has zero elements", sliceValidateError{}, ""},
|
{"has zero elements", SliceValidationError{}, ""},
|
||||||
{"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
|
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
||||||
{"has two elements",
|
{"has two elements",
|
||||||
sliceValidateError{
|
SliceValidationError{
|
||||||
errors.New("first error"),
|
errors.New("first error"),
|
||||||
errors.New("second error"),
|
errors.New("second error"),
|
||||||
},
|
},
|
||||||
"[0]: first error\n[1]: second error",
|
"[0]: first error\n[1]: second error",
|
||||||
},
|
},
|
||||||
{"has many elements",
|
{"has many elements",
|
||||||
sliceValidateError{
|
SliceValidationError{
|
||||||
errors.New("first error"),
|
errors.New("first error"),
|
||||||
errors.New("second error"),
|
errors.New("second error"),
|
||||||
nil,
|
nil,
|
||||||
|
@ -40,7 +40,7 @@ func TestSliceValidateError(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := tt.err.Error(); got != tt.want {
|
if got := tt.err.Error(); got != tt.want {
|
||||||
t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
|
t.Errorf("SliceValidationError.Error() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
74
context.go
74
context.go
|
@ -59,7 +59,7 @@ type Context struct {
|
||||||
params *Params
|
params *Params
|
||||||
skippedNodes *[]skippedNode
|
skippedNodes *[]skippedNode
|
||||||
|
|
||||||
// This mutex protect Keys map
|
// This mutex protects Keys map.
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
// Keys is a key/value pair exclusively for the context of each request.
|
// Keys is a key/value pair exclusively for the context of each request.
|
||||||
|
@ -71,10 +71,10 @@ type Context struct {
|
||||||
// Accepted defines a list of manually accepted formats for content negotiation.
|
// Accepted defines a list of manually accepted formats for content negotiation.
|
||||||
Accepted []string
|
Accepted []string
|
||||||
|
|
||||||
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
|
// queryCache caches the query result from c.Request.URL.Query().
|
||||||
queryCache url.Values
|
queryCache url.Values
|
||||||
|
|
||||||
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
|
// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
|
||||||
// or PUT body parameters.
|
// or PUT body parameters.
|
||||||
formCache url.Values
|
formCache url.Values
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ func (c *Context) Set(key string, value interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the value for the given key, ie: (value, true).
|
// Get returns the value for the given key, ie: (value, true).
|
||||||
// If the value does not exists it returns (nil, false)
|
// If the value does not exist it returns (nil, false)
|
||||||
func (c *Context) Get(key string) (value interface{}, exists bool) {
|
func (c *Context) Get(key string) (value interface{}, exists bool) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
value, exists = c.Keys[key]
|
value, exists = c.Keys[key]
|
||||||
|
@ -602,7 +602,7 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind checks the Content-Type to select a binding engine automatically,
|
// Bind checks the Content-Type to select a binding engine automatically,
|
||||||
// Depending the "Content-Type" header different bindings are used:
|
// Depending on the "Content-Type" header different bindings are used:
|
||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML binding
|
// "application/xml" --> XML binding
|
||||||
// otherwise --> returns an error.
|
// otherwise --> returns an error.
|
||||||
|
@ -661,7 +661,7 @@ func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
||||||
// Depending the "Content-Type" header different bindings are used:
|
// Depending on the "Content-Type" header different bindings are used:
|
||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML binding
|
// "application/xml" --> XML binding
|
||||||
// otherwise --> returns an error
|
// otherwise --> returns an error
|
||||||
|
@ -739,7 +739,7 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
|
||||||
// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
|
// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
|
||||||
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
|
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
|
||||||
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
|
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
|
||||||
// the remote IP (coming form Request.RemoteAddr) is returned.
|
// the remote IP (coming from Request.RemoteAddr) is returned.
|
||||||
func (c *Context) ClientIP() string {
|
func (c *Context) ClientIP() string {
|
||||||
// Check if we're running on a trusted platform, continue running backwards if error
|
// Check if we're running on a trusted platform, continue running backwards if error
|
||||||
if c.engine.TrustedPlatform != "" {
|
if c.engine.TrustedPlatform != "" {
|
||||||
|
@ -757,10 +757,14 @@ func (c *Context) ClientIP() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteIP, trusted := c.RemoteIP()
|
// It also checks if the remoteIP is a trusted proxy or not.
|
||||||
|
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
||||||
|
// defined by Engine.SetTrustedProxies()
|
||||||
|
remoteIP := net.ParseIP(c.RemoteIP())
|
||||||
if remoteIP == nil {
|
if remoteIP == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
trusted := c.engine.isTrustedProxy(remoteIP)
|
||||||
|
|
||||||
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||||
|
@ -773,53 +777,13 @@ func (c *Context) ClientIP() string {
|
||||||
return remoteIP.String()
|
return remoteIP.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) isTrustedProxy(ip net.IP) bool {
|
|
||||||
if e.trustedCIDRs != nil {
|
|
||||||
for _, cidr := range e.trustedCIDRs {
|
|
||||||
if cidr.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
|
// 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.
|
func (c *Context) RemoteIP() string {
|
||||||
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
|
||||||
// defined by Engine.SetTrustedProxies()
|
|
||||||
func (c *Context) RemoteIP() (net.IP, bool) {
|
|
||||||
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return ""
|
||||||
}
|
}
|
||||||
remoteIP := net.ParseIP(ip)
|
return ip
|
||||||
if remoteIP == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteIP, c.engine.isTrustedProxy(remoteIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
|
|
||||||
if header == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
items := strings.Split(header, ",")
|
|
||||||
for i := len(items) - 1; i >= 0; i-- {
|
|
||||||
ipStr := strings.TrimSpace(items[i])
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// X-Forwarded-For is appended by proxy
|
|
||||||
// Check IPs in reverse order and stop when find untrusted proxy
|
|
||||||
if (i == 0) || (!e.isTrustedProxy(ip)) {
|
|
||||||
return ipStr, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentType returns the Content-Type header of the request.
|
// ContentType returns the Content-Type header of the request.
|
||||||
|
@ -863,7 +827,7 @@ func (c *Context) Status(code int) {
|
||||||
c.Writer.WriteHeader(code)
|
c.Writer.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
|
// Header is an intelligent shortcut for c.Writer.Header().Set(key, value).
|
||||||
// It writes a header in the response.
|
// It writes a header in the response.
|
||||||
// If value == "", this method removes the header `c.Writer.Header().Del(key)`
|
// If value == "", this method removes the header `c.Writer.Header().Del(key)`
|
||||||
func (c *Context) Header(key, value string) {
|
func (c *Context) Header(key, value string) {
|
||||||
|
@ -946,7 +910,7 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
|
||||||
|
|
||||||
// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
|
// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
|
||||||
// It also sets the Content-Type as "application/json".
|
// It also sets the Content-Type as "application/json".
|
||||||
// WARNING: we recommend to use this only for development purposes since printing pretty JSON is
|
// WARNING: we recommend using this only for development purposes since printing pretty JSON is
|
||||||
// more CPU and bandwidth consuming. Use Context.JSON() instead.
|
// more CPU and bandwidth consuming. Use Context.JSON() instead.
|
||||||
func (c *Context) IndentedJSON(code int, obj interface{}) {
|
func (c *Context) IndentedJSON(code int, obj interface{}) {
|
||||||
c.Render(code, render.IndentedJSON{Data: obj})
|
c.Render(code, render.IndentedJSON{Data: obj})
|
||||||
|
@ -1010,7 +974,7 @@ func (c *Context) String(code int, format string, values ...interface{}) {
|
||||||
c.Render(code, render.String{Format: format, Data: values})
|
c.Render(code, render.String{Format: format, Data: values})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect returns a HTTP redirect to the specific location.
|
// Redirect returns an HTTP redirect to the specific location.
|
||||||
func (c *Context) Redirect(code int, location string) {
|
func (c *Context) Redirect(code int, location string) {
|
||||||
c.Render(-1, render.Redirect{
|
c.Render(-1, render.Redirect{
|
||||||
Code: code,
|
Code: code,
|
||||||
|
@ -1102,7 +1066,7 @@ type Negotiate struct {
|
||||||
Data interface{}
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negotiate calls different Render according acceptable Accept format.
|
// Negotiate calls different Render according to acceptable Accept format.
|
||||||
func (c *Context) Negotiate(code int, config Negotiate) {
|
func (c *Context) Negotiate(code int, config Negotiate) {
|
||||||
switch c.NegotiateFormat(config.Offered...) {
|
switch c.NegotiateFormat(config.Offered...) {
|
||||||
case binding.MIMEJSON:
|
case binding.MIMEJSON:
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
@ -1404,6 +1405,11 @@ func TestContextClientIP(t *testing.T) {
|
||||||
// Tests exercising the TrustedProxies functionality
|
// Tests exercising the TrustedProxies functionality
|
||||||
resetContextForClientIPTests(c)
|
resetContextForClientIPTests(c)
|
||||||
|
|
||||||
|
// IPv6 support
|
||||||
|
c.Request.RemoteAddr = "[::1]:12345"
|
||||||
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
// No trusted proxies
|
// No trusted proxies
|
||||||
_ = c.engine.SetTrustedProxies([]string{})
|
_ = c.engine.SetTrustedProxies([]string{})
|
||||||
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
@ -1500,6 +1506,7 @@ func resetContextForClientIPTests(c *Context) {
|
||||||
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
|
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
|
||||||
c.Request.RemoteAddr = " 40.40.40.40:42123 "
|
c.Request.RemoteAddr = " 40.40.40.40:42123 "
|
||||||
c.engine.TrustedPlatform = ""
|
c.engine.TrustedPlatform = ""
|
||||||
|
c.engine.trustedCIDRs = defaultTrustedCIDRs
|
||||||
c.engine.AppEngine = false
|
c.engine.AppEngine = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2051,7 +2058,8 @@ func TestRemoteIPFail(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
c.Request.RemoteAddr = "[:::]:80"
|
c.Request.RemoteAddr = "[:::]:80"
|
||||||
ip, trust := c.RemoteIP()
|
ip := net.ParseIP(c.RemoteIP())
|
||||||
|
trust := c.engine.isTrustedProxy(ip)
|
||||||
assert.Nil(t, ip)
|
assert.Nil(t, ip)
|
||||||
assert.False(t, trust)
|
assert.False(t, trust)
|
||||||
}
|
}
|
||||||
|
|
73
gin.go
73
gin.go
|
@ -11,7 +11,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
@ -28,15 +27,24 @@ var (
|
||||||
|
|
||||||
var defaultPlatform string
|
var defaultPlatform string
|
||||||
|
|
||||||
var defaultTrustedCIDRs = []*net.IPNet{{IP: net.IP{0x0, 0x0, 0x0, 0x0}, Mask: net.IPMask{0x0, 0x0, 0x0, 0x0}}} // 0.0.0.0/0
|
var defaultTrustedCIDRs = []*net.IPNet{
|
||||||
|
{ // 0.0.0.0/0 (IPv4)
|
||||||
|
IP: net.IP{0x0, 0x0, 0x0, 0x0},
|
||||||
|
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0},
|
||||||
|
},
|
||||||
|
{ // ::/0 (IPv6)
|
||||||
|
IP: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||||
type HandlerFunc func(*Context)
|
type HandlerFunc func(*Context)
|
||||||
|
|
||||||
// HandlersChain defines a HandlerFunc array.
|
// HandlersChain defines a HandlerFunc slice.
|
||||||
type HandlersChain []HandlerFunc
|
type HandlersChain []HandlerFunc
|
||||||
|
|
||||||
// Last returns the last handler in the chain. ie. the last handler is the main one.
|
// Last returns the last handler in the chain. i.e. the last handler is the main one.
|
||||||
func (c HandlersChain) Last() HandlerFunc {
|
func (c HandlersChain) Last() HandlerFunc {
|
||||||
if length := len(c); length > 0 {
|
if length := len(c); length > 0 {
|
||||||
return c[length-1]
|
return c[length-1]
|
||||||
|
@ -52,7 +60,7 @@ type RouteInfo struct {
|
||||||
HandlerFunc HandlerFunc
|
HandlerFunc HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoutesInfo defines a RouteInfo array.
|
// RoutesInfo defines a RouteInfo slice.
|
||||||
type RoutesInfo []RouteInfo
|
type RoutesInfo []RouteInfo
|
||||||
|
|
||||||
// Trusted platforms
|
// Trusted platforms
|
||||||
|
@ -102,7 +110,7 @@ type Engine struct {
|
||||||
// `(*gin.Context).Request.RemoteAddr`.
|
// `(*gin.Context).Request.RemoteAddr`.
|
||||||
ForwardedByClientIP bool
|
ForwardedByClientIP bool
|
||||||
|
|
||||||
// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
|
// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
|
||||||
// #726 #755 If enabled, it will trust some headers starting with
|
// #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
|
||||||
|
@ -154,7 +162,7 @@ type Engine struct {
|
||||||
var _ IRouter = &Engine{}
|
var _ IRouter = &Engine{}
|
||||||
|
|
||||||
// New returns a new blank Engine instance without any middleware attached.
|
// New returns a new blank Engine instance without any middleware attached.
|
||||||
// By default the configuration is:
|
// By default, the configuration is:
|
||||||
// - RedirectTrailingSlash: true
|
// - RedirectTrailingSlash: true
|
||||||
// - RedirectFixedPath: false
|
// - RedirectFixedPath: false
|
||||||
// - HandleMethodNotAllowed: false
|
// - HandleMethodNotAllowed: false
|
||||||
|
@ -207,7 +215,7 @@ func (engine *Engine) allocateContext() *Context {
|
||||||
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
|
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delims sets template left and right delims and returns a Engine instance.
|
// Delims sets template left and right delims and returns an Engine instance.
|
||||||
func (engine *Engine) Delims(left, right string) *Engine {
|
func (engine *Engine) Delims(left, right string) *Engine {
|
||||||
engine.delims = render.Delims{Left: left, Right: right}
|
engine.delims = render.Delims{Left: left, Right: right}
|
||||||
return engine
|
return engine
|
||||||
|
@ -261,7 +269,7 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
|
||||||
engine.FuncMap = funcMap
|
engine.FuncMap = funcMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
|
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
|
||||||
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
||||||
engine.noRoute = handlers
|
engine.noRoute = handlers
|
||||||
engine.rebuild404Handlers()
|
engine.rebuild404Handlers()
|
||||||
|
@ -280,7 +288,7 @@ func (engine *Engine) AutoRedirect(handlers ...HandlerFunc) {
|
||||||
engine.rebuildAutoRedirectHandlers()
|
engine.rebuildAutoRedirectHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
|
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
|
||||||
// included in the handlers chain for every single request. Even 404, 405, static files...
|
// included in the handlers chain for every single request. Even 404, 405, static files...
|
||||||
// For example, this is the right place for a logger or error management middleware.
|
// For example, this is the right place for a logger or error management middleware.
|
||||||
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||||
|
@ -413,9 +421,9 @@ func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
|
||||||
return engine.parseTrustedProxies()
|
return engine.parseTrustedProxies()
|
||||||
}
|
}
|
||||||
|
|
||||||
// isUnsafeTrustedProxies compares Engine.trustedCIDRs and defaultTrustedCIDRs, it's not safe if equal (returns true)
|
// isUnsafeTrustedProxies checks if Engine.trustedCIDRs contains all IPs, it's not safe if it has (returns true)
|
||||||
func (engine *Engine) isUnsafeTrustedProxies() bool {
|
func (engine *Engine) isUnsafeTrustedProxies() bool {
|
||||||
return reflect.DeepEqual(engine.trustedCIDRs, defaultTrustedCIDRs)
|
return engine.isTrustedProxy(net.ParseIP("0.0.0.0")) || engine.isTrustedProxy(net.ParseIP("::"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
|
// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
|
||||||
|
@ -425,6 +433,41 @@ func (engine *Engine) parseTrustedProxies() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isTrustedProxy will check whether the IP address is included in the trusted list according to Engine.trustedCIDRs
|
||||||
|
func (engine *Engine) isTrustedProxy(ip net.IP) bool {
|
||||||
|
if engine.trustedCIDRs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, cidr := range engine.trustedCIDRs {
|
||||||
|
if cidr.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateHeader will parse X-Forwarded-For header and return the trusted client IP address
|
||||||
|
func (engine *Engine) validateHeader(header string) (clientIP string, valid bool) {
|
||||||
|
if header == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
items := strings.Split(header, ",")
|
||||||
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
|
ipStr := strings.TrimSpace(items[i])
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-Forwarded-For is appended by proxy
|
||||||
|
// Check IPs in reverse order and stop when find untrusted proxy
|
||||||
|
if (i == 0) || (!engine.isTrustedProxy(ip)) {
|
||||||
|
return ipStr, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
// parseIP parse a string representation of an IP and returns a net.IP with the
|
// parseIP parse a string representation of an IP and returns a net.IP with the
|
||||||
// minimum byte representation or nil if input is invalid.
|
// minimum byte representation or nil if input is invalid.
|
||||||
func parseIP(ip string) net.IP {
|
func parseIP(ip string) net.IP {
|
||||||
|
@ -456,7 +499,7 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
|
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||||
// through the specified unix socket (ie. a file).
|
// through the specified unix socket (i.e. a file).
|
||||||
// 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.
|
||||||
func (engine *Engine) RunUnix(file string) (err error) {
|
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)
|
||||||
|
@ -527,9 +570,9 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
engine.pool.Put(c)
|
engine.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleContext re-enter a context that has been rewritten.
|
// HandleContext re-enters a context that has been rewritten.
|
||||||
// This can be done by setting c.Request.URL.Path to your new target.
|
// This can be done by setting c.Request.URL.Path to your new target.
|
||||||
// Disclaimer: You can loop yourself to death with this, use wisely.
|
// Disclaimer: You can loop yourself to deal with this, use wisely.
|
||||||
func (engine *Engine) HandleContext(c *Context) {
|
func (engine *Engine) HandleContext(c *Context) {
|
||||||
oldIndexValue := c.index
|
oldIndexValue := c.index
|
||||||
c.reset()
|
c.reset()
|
||||||
|
|
|
@ -37,7 +37,7 @@ func SetHTMLTemplate(templ *template.Template) {
|
||||||
engine().SetHTMLTemplate(templ)
|
engine().SetHTMLTemplate(templ)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
|
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
|
||||||
func NoRoute(handlers ...gin.HandlerFunc) {
|
func NoRoute(handlers ...gin.HandlerFunc) {
|
||||||
engine().NoRoute(handlers...)
|
engine().NoRoute(handlers...)
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
|
||||||
return engine().StaticFS(relativePath, fs)
|
return engine().StaticFS(relativePath, fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
|
// Use attaches a global middleware to the router. i.e. the middlewares attached though Use() will be
|
||||||
// included in the handlers chain for every single request. Even 404, 405, static files...
|
// included in the handlers chain for every single request. Even 404, 405, static files...
|
||||||
// For example, this is the right place for a logger or error management middleware.
|
// For example, this is the right place for a logger or error management middleware.
|
||||||
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
||||||
|
@ -145,7 +145,7 @@ func RunTLS(addr, certFile, keyFile string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
|
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
|
||||||
// through the specified unix socket (ie. a file)
|
// through the specified unix socket (i.e. a file)
|
||||||
// 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.
|
||||||
func RunUnix(file string) (err error) {
|
func RunUnix(file string) (err error) {
|
||||||
return engine().RunUnix(file)
|
return engine().RunUnix(file)
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -5,7 +5,7 @@ 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.9.0
|
github.com/go-playground/validator/v10 v10.9.0
|
||||||
github.com/goccy/go-json v0.7.10
|
github.com/goccy/go-json v0.8.1
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/mattn/go-isatty v0.0.14
|
github.com/mattn/go-isatty v0.0.14
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
@ -13,3 +13,5 @@ require (
|
||||||
google.golang.org/protobuf v1.27.1
|
google.golang.org/protobuf v1.27.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
retract v1.7.5
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -12,8 +12,8 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
|
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
|
||||||
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec=
|
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
|
||||||
github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
|
|
@ -70,7 +70,7 @@ type LogFormatterParams struct {
|
||||||
Path string
|
Path string
|
||||||
// ErrorMessage is set if error has occurred in processing the request.
|
// ErrorMessage is set if error has occurred in processing the request.
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
// isTerm shows whether does gin's output descriptor refers to a terminal.
|
// isTerm shows whether gin's output descriptor refers to a terminal.
|
||||||
isTerm bool
|
isTerm bool
|
||||||
// BodySize is the size of the Response Body
|
// BodySize is the size of the Response Body
|
||||||
BodySize int
|
BodySize int
|
||||||
|
@ -178,7 +178,7 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
||||||
// By default gin.DefaultWriter = os.Stdout.
|
// By default, gin.DefaultWriter = os.Stdout.
|
||||||
func Logger() HandlerFunc {
|
func Logger() HandlerFunc {
|
||||||
return LoggerWithConfig(LoggerConfig{})
|
return LoggerWithConfig(LoggerConfig{})
|
||||||
}
|
}
|
||||||
|
|
2
mode.go
2
mode.go
|
@ -88,7 +88,7 @@ func EnableJsonDecoderDisallowUnknownFields() {
|
||||||
binding.EnableDecoderDisallowUnknownFields = true
|
binding.EnableDecoderDisallowUnknownFields = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns currently gin mode.
|
// Mode returns current gin mode.
|
||||||
func Mode() string {
|
func Mode() string {
|
||||||
return modeName
|
return modeName
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@ func function(pc uintptr) []byte {
|
||||||
// runtime/debug.*T·ptrmethod
|
// runtime/debug.*T·ptrmethod
|
||||||
// and want
|
// and want
|
||||||
// *T.ptrmethod
|
// *T.ptrmethod
|
||||||
// Also the package path might contains dot (e.g. code.google.com/...),
|
// Also the package path might contain dot (e.g. code.google.com/...),
|
||||||
// so first eliminate the path prefix
|
// so first eliminate the path prefix
|
||||||
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
||||||
name = name[lastSlash+1:]
|
name = name[lastSlash+1:]
|
||||||
|
|
9
tree.go
9
tree.go
|
@ -349,7 +349,12 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||||
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||||
|
panic("catch-all wildcard '" + path +
|
||||||
|
"' in new path '" + fullPath +
|
||||||
|
"' conflicts with existing path segment '" + pathSeg +
|
||||||
|
"' in existing prefix '" + n.path + pathSeg +
|
||||||
|
"'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently fixed width 1 for '/'
|
// currently fixed width 1 for '/'
|
||||||
|
@ -530,7 +535,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) || (n.path == "" && n.indices == "/")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -595,6 +595,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
||||||
"/blog/:p",
|
"/blog/:p",
|
||||||
"/posts/:b/:c",
|
"/posts/:b/:c",
|
||||||
"/posts/b/:c/d/",
|
"/posts/b/:c/d/",
|
||||||
|
"/vendor/:x/*y",
|
||||||
}
|
}
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
recv := catchPanic(func() {
|
recv := catchPanic(func() {
|
||||||
|
@ -631,6 +632,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
||||||
"/api/world/abc/",
|
"/api/world/abc/",
|
||||||
"/blog/pp/",
|
"/blog/pp/",
|
||||||
"/posts/b/c/d",
|
"/posts/b/c/d",
|
||||||
|
"/vendor/x",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range tsrRoutes {
|
for _, route := range tsrRoutes {
|
||||||
|
|
|
@ -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.7.4"
|
const Version = "v1.7.7"
|
||||||
|
|
Loading…
Reference in New Issue