mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-24 22:55:45 +08:00
Compare commits
5 Commits
51354e244f
...
7bbfea7d9c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bbfea7d9c | ||
|
|
771dcc6476 | ||
|
|
52ecf029bd | ||
|
|
440eb14ab8 | ||
|
|
322da9ca24 |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
4
.github/workflows/gin.yml
vendored
4
.github/workflows/gin.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
|
||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
|
||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
26
context.go
26
context.go
@ -280,7 +280,7 @@ func (c *Context) Error(err error) *Error {
|
||||
|
||||
// Set is used to store a new key/value pair exclusively for this context.
|
||||
// It also lazy initializes c.Keys if it was not used previously.
|
||||
func (c *Context) Set(key any, value any) {
|
||||
func (c *Context) Set(key any, value any) *Context {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys == nil {
|
||||
@ -288,6 +288,7 @@ func (c *Context) Set(key any, value any) {
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
return c
|
||||
}
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
@ -506,8 +507,9 @@ func (c *Context) Param(key string) string {
|
||||
// Example Route: "/user/:id"
|
||||
// AddParam("id", 1)
|
||||
// Result: "/user/1"
|
||||
func (c *Context) AddParam(key, value string) {
|
||||
func (c *Context) AddParam(key, value string) *Context {
|
||||
c.Params = append(c.Params, Param{Key: key, Value: value})
|
||||
return c
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists,
|
||||
@ -1052,19 +1054,21 @@ func bodyAllowedForStatus(status int) bool {
|
||||
}
|
||||
|
||||
// Status sets the HTTP response code.
|
||||
func (c *Context) Status(code int) {
|
||||
func (c *Context) Status(code int) *Context {
|
||||
c.Writer.WriteHeader(code)
|
||||
return c
|
||||
}
|
||||
|
||||
// Header is an intelligent shortcut for c.Writer.Header().Set(key, value).
|
||||
// It writes a header in the response.
|
||||
// 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) *Context {
|
||||
if value == "" {
|
||||
c.Writer.Header().Del(key)
|
||||
return
|
||||
return c
|
||||
}
|
||||
c.Writer.Header().Set(key, value)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetHeader returns value from request headers.
|
||||
@ -1081,14 +1085,15 @@ func (c *Context) GetRawData() ([]byte, error) {
|
||||
}
|
||||
|
||||
// SetSameSite with cookie
|
||||
func (c *Context) SetSameSite(samesite http.SameSite) {
|
||||
func (c *Context) SetSameSite(samesite http.SameSite) *Context {
|
||||
c.sameSite = samesite
|
||||
return c
|
||||
}
|
||||
|
||||
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be
|
||||
// silently dropped.
|
||||
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
|
||||
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) *Context {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
@ -1102,12 +1107,13 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
|
||||
Secure: secure,
|
||||
HttpOnly: httpOnly,
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
|
||||
func (c *Context) SetCookieData(cookie *http.Cookie) {
|
||||
func (c *Context) SetCookieData(cookie *http.Cookie) *Context {
|
||||
if cookie.Path == "" {
|
||||
cookie.Path = "/"
|
||||
}
|
||||
@ -1115,6 +1121,7 @@ func (c *Context) SetCookieData(cookie *http.Cookie) {
|
||||
cookie.SameSite = c.sameSite
|
||||
}
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
return c
|
||||
}
|
||||
|
||||
// Cookie returns the named cookie provided in the request or
|
||||
@ -1394,8 +1401,9 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
||||
}
|
||||
|
||||
// SetAccepted sets Accept header data.
|
||||
func (c *Context) SetAccepted(formats ...string) {
|
||||
func (c *Context) SetAccepted(formats ...string) *Context {
|
||||
c.Accepted = formats
|
||||
return c
|
||||
}
|
||||
|
||||
/************************************/
|
||||
|
||||
@ -3677,3 +3677,46 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextChaining(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
// Basic cookie settings
|
||||
cookie := &http.Cookie{
|
||||
Name: "name",
|
||||
Value: "gin",
|
||||
MaxAge: 1,
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
c.Set("foo", "bar").
|
||||
AddParam("id", "1").
|
||||
SetSameSite(http.SameSiteLaxMode).
|
||||
SetCookie("user", "gin", 1, "/", "localhost", true, true).
|
||||
SetCookieData(cookie).
|
||||
Header("Content-Type", "text/plain").
|
||||
Header("X-Custom", "value").
|
||||
SetAccepted(MIMEJSON, MIMEXML).
|
||||
Status(200)
|
||||
|
||||
value, err := c.Get("foo")
|
||||
assert.Equal(t, "bar", value)
|
||||
assert.True(t, err)
|
||||
|
||||
v, ok := c.Params.Get("id")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "1", v)
|
||||
|
||||
assert.Equal(t, []string{"user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", "name=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure"}, c.Writer.Header().Values("Set-Cookie"))
|
||||
|
||||
assert.Equal(t, "text/plain", c.Writer.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "value", c.Writer.Header().Get("X-Custom"))
|
||||
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) //nolint:testifylint
|
||||
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML, MIMEHTML))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON)) //nolint:testifylint
|
||||
|
||||
assert.Equal(t, 200, c.Writer.Status())
|
||||
}
|
||||
|
||||
37
gin.go
37
gin.go
@ -11,7 +11,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -48,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
)
|
||||
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
@ -141,10 +135,16 @@ type Engine struct {
|
||||
AppEngine bool
|
||||
|
||||
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
||||
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
|
||||
// Only use RawPath if you know what you are doing.
|
||||
UseRawPath bool
|
||||
|
||||
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
|
||||
// It overrides UseRawPath
|
||||
UseEscapedPath bool
|
||||
|
||||
// UnescapePathValues if true, the path value will be unescaped.
|
||||
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
||||
// If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
|
||||
// as url.Path gonna be used, which is already unescaped.
|
||||
UnescapePathValues bool
|
||||
|
||||
@ -197,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
|
||||
// - HandleMethodNotAllowed: false
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UseEscapedPath: false
|
||||
// - UnescapePathValues: true
|
||||
func New(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGNew()
|
||||
@ -214,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||
TrustedPlatform: defaultPlatform,
|
||||
UseRawPath: false,
|
||||
UseEscapedPath: false,
|
||||
RemoveExtraSlash: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
@ -689,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
httpMethod := c.Request.Method
|
||||
rPath := c.Request.URL.Path
|
||||
unescape := false
|
||||
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
|
||||
if engine.UseEscapedPath {
|
||||
rPath = c.Request.URL.EscapedPath()
|
||||
unescape = engine.UnescapePathValues
|
||||
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
rPath = c.Request.URL.RawPath
|
||||
unescape = engine.UnescapePathValues
|
||||
}
|
||||
@ -776,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||
prefix = sanitizePathChars(prefix)
|
||||
prefix = removeRepeatedChar(prefix, '/')
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
@ -788,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
// sanitizePathChars removes unsafe characters from path strings,
|
||||
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
|
||||
func sanitizePathChars(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
rPath := req.URL.Path
|
||||
|
||||
49
gin_test.go
49
gin_test.go
@ -720,6 +720,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
|
||||
assert.Equal(t, int64(1), handlerCounterV2)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is Escaped, the %25 is not interpreted as %
|
||||
assert.Equal(t, "foo%252Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is used, the %25 is interpreted as %
|
||||
assert.Equal(t, "foo%2Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
assert.Equal(t, "foo%25bar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
|
||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
||||
|
||||
package gin
|
||||
|
||||
const stackBufSize = 128
|
||||
|
||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
@ -19,7 +21,6 @@ package gin
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
||||
// removeRepeatedChar removes multiple consecutive 'char's from a string.
|
||||
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
|
||||
func removeRepeatedChar(s string, char byte) string {
|
||||
// Check if there are any consecutive chars
|
||||
hasRepeatedChar := false
|
||||
for i := 1; i < len(s); i++ {
|
||||
if s[i] == char && s[i-1] == char {
|
||||
hasRepeatedChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRepeatedChar {
|
||||
return s
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
// Invariants:
|
||||
// reading from s; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
r := 0
|
||||
w := 0
|
||||
|
||||
for n := len(s); r < n; {
|
||||
if s[r] == char {
|
||||
// Write the first char
|
||||
bufApp(&buf, s, w, char)
|
||||
w++
|
||||
r++
|
||||
|
||||
// Skip all consecutive chars
|
||||
for r < n && s[r] == char {
|
||||
r++
|
||||
}
|
||||
} else {
|
||||
// Copy non-char character
|
||||
bufApp(&buf, s, w, s[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise, return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return s[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
47
path_test.go
47
path_test.go
@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepeatedChar(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
str string
|
||||
char byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
str: "",
|
||||
char: 'a',
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "noSlash",
|
||||
str: "abc",
|
||||
char: ',',
|
||||
want: "abc",
|
||||
},
|
||||
{
|
||||
name: "withSlash",
|
||||
str: "/a/b/c/",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "withRepeatedSlashes",
|
||||
str: "/a//b///c////",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "threeSlashes",
|
||||
str: "///",
|
||||
char: '/',
|
||||
want: "/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := removeRepeatedChar(tc.str, tc.char)
|
||||
assert.Equal(t, tc.want, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user