mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-12 16:48:19 +08:00
Compare commits
6 Commits
5364c7b06b
...
5cede4593d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cede4593d | ||
|
|
b2b489dbf4 | ||
|
|
3ab698dc51 | ||
|
|
39122a594e | ||
|
|
d0c802af21 | ||
|
|
322da9ca24 |
53
context.go
53
context.go
@ -272,7 +272,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 {
|
||||
@ -280,6 +280,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).
|
||||
@ -498,8 +499,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,
|
||||
@ -978,14 +980,27 @@ func (c *Context) ClientIP() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return ""
|
||||
var (
|
||||
trusted bool
|
||||
remoteIP net.IP
|
||||
)
|
||||
// If gin is listening a unix socket, always trust it.
|
||||
localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
|
||||
trusted = true
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if !trusted {
|
||||
// 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 {
|
||||
return ""
|
||||
}
|
||||
trusted = c.engine.isTrustedProxy(remoteIP)
|
||||
}
|
||||
trusted := c.engine.isTrustedProxy(remoteIP)
|
||||
|
||||
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||
@ -1045,19 +1060,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.
|
||||
@ -1074,14 +1091,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 = "/"
|
||||
}
|
||||
@ -1095,12 +1113,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 = "/"
|
||||
}
|
||||
@ -1108,6 +1127,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
|
||||
@ -1387,8 +1407,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
|
||||
}
|
||||
|
||||
/************************************/
|
||||
|
||||
@ -1915,6 +1915,16 @@ func TestContextClientIP(t *testing.T) {
|
||||
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
|
||||
resetContextForClientIPTests(c)
|
||||
|
||||
// unix address
|
||||
addr := &net.UnixAddr{Net: "unix", Name: "@"}
|
||||
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr))
|
||||
c.Request.RemoteAddr = addr.String()
|
||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||
|
||||
// reset
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
resetContextForClientIPTests(c)
|
||||
|
||||
// Legacy tests (validating that the defaults don't break the
|
||||
// (insecure!) old behaviour)
|
||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||
@ -3708,3 +3718,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
recovery.go
37
recovery.go
@ -12,12 +12,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
@ -57,40 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if rec := recover(); rec != nil {
|
||||
// Check for a broken connection, as it is not really a
|
||||
// condition that warrants a panic stack trace.
|
||||
var brokenPipe bool
|
||||
if ne, ok := err.(*net.OpError); ok {
|
||||
var se *os.SyscallError
|
||||
if errors.As(ne, &se) {
|
||||
seStr := strings.ToLower(se.Error())
|
||||
if strings.Contains(seStr, "broken pipe") ||
|
||||
strings.Contains(seStr, "connection reset by peer") {
|
||||
brokenPipe = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
|
||||
brokenPipe = true
|
||||
var isBrokenPipe bool
|
||||
err, ok := rec.(error)
|
||||
if ok {
|
||||
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
|
||||
errors.Is(err, syscall.ECONNRESET) ||
|
||||
errors.Is(err, http.ErrAbortHandler)
|
||||
}
|
||||
if logger != nil {
|
||||
if brokenPipe {
|
||||
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
||||
if isBrokenPipe {
|
||||
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
|
||||
} else if IsDebugging() {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||
timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
|
||||
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
|
||||
} else {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||
timeFormat(time.Now()), err, stack(stackSkip), reset)
|
||||
timeFormat(time.Now()), rec, stack(stackSkip), reset)
|
||||
}
|
||||
}
|
||||
if brokenPipe {
|
||||
if isBrokenPipe {
|
||||
// If the connection is dead, we can't write a status to it.
|
||||
c.Error(err.(error)) //nolint: errcheck
|
||||
c.Error(err) //nolint: errcheck
|
||||
c.Abort()
|
||||
} else {
|
||||
handle(c, err)
|
||||
handle(c, rec)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@ -98,13 +98,13 @@ func TestFunction(t *testing.T) {
|
||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
expectMsgs := map[syscall.Errno]string{
|
||||
syscall.EPIPE: "broken pipe",
|
||||
syscall.ECONNRESET: "connection reset by peer",
|
||||
expectErrnos := []syscall.Errno{
|
||||
syscall.EPIPE,
|
||||
syscall.ECONNRESET,
|
||||
}
|
||||
|
||||
for errno, expectMsg := range expectMsgs {
|
||||
t.Run(expectMsg, func(t *testing.T) {
|
||||
for _, errno := range expectErrnos {
|
||||
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
|
||||
router := New()
|
||||
@ -122,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, expectCode, w.Code)
|
||||
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
||||
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
|
||||
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user