Merge 4eecebf8bfcfdf2bde13f2757931a570e4d38529 into 8763f33c65f7df8be5b9fe7504ab7fcf20abb41d

This commit is contained in:
bound2 2025-03-28 09:14:21 +02:00 committed by GitHub
commit bd1d2ec7c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 51 additions and 9 deletions

View File

@ -6,9 +6,11 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log" "log"
"log/slog"
"math" "math"
"mime/multipart" "mime/multipart"
"net" "net"
@ -47,6 +49,20 @@ const ContextKey = "_gin-gonic/gin/contextkey"
type ContextKeyType int type ContextKeyType int
type CookieOption func(*http.Cookie)
func WithPartitionedCookie(partitioned bool) CookieOption {
return func(cookie *http.Cookie) {
cookie.Partitioned = partitioned
}
}
func WithSameSiteCookie(sameSite http.SameSite) CookieOption {
return func(cookie *http.Cookie) {
cookie.SameSite = sameSite
}
}
const ContextRequestKey ContextKeyType = 0 const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions. // abortIndex represents a typical value used in abort functions.
@ -87,9 +103,9 @@ type Context struct {
// or PUT body parameters. // or PUT body parameters.
formCache url.Values formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for // cookieOptions set up additional cookie parameters to be used with http.Cookie
// the browser to send this cookie along with cross-site requests. // this is needed to prevent breaking existing implementations
sameSite http.SameSite cookieOptions []CookieOption
} }
/************************************/ /************************************/
@ -108,7 +124,7 @@ func (c *Context) reset() {
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
c.formCache = nil c.formCache = nil
c.sameSite = 0 c.cookieOptions = make([]CookieOption, 0)
*c.params = (*c.params)[:0] *c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0] *c.skippedNodes = (*c.skippedNodes)[:0]
} }
@ -1005,26 +1021,38 @@ func (c *Context) GetRawData() ([]byte, error) {
// SetSameSite with cookie // SetSameSite with cookie
func (c *Context) SetSameSite(samesite http.SameSite) { func (c *Context) SetSameSite(samesite http.SameSite) {
c.sameSite = samesite c.cookieOptions = append(c.cookieOptions, WithSameSiteCookie(samesite))
} }
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers. // SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be // The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped. // 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, options ...CookieOption) {
if path == "" { if path == "" {
path = "/" path = "/"
} }
http.SetCookie(c.Writer, &http.Cookie{
cookie := &http.Cookie{
Name: name, Name: name,
Value: url.QueryEscape(value), Value: url.QueryEscape(value),
MaxAge: maxAge, MaxAge: maxAge,
Path: path, Path: path,
Domain: domain, Domain: domain,
SameSite: c.sameSite,
Secure: secure, Secure: secure,
HttpOnly: httpOnly, HttpOnly: httpOnly,
}) }
for _, option := range c.cookieOptions {
option(cookie)
}
for _, option := range options {
option(cookie)
}
if err := cookie.Valid(); err != nil {
slog.Error(fmt.Sprintf("invalid cookie: %v", err))
}
http.SetCookie(c.Writer, cookie)
} }
// Cookie returns the named cookie provided in the request or // Cookie returns the named cookie provided in the request or

View File

@ -206,6 +206,7 @@ func TestContextReset(t *testing.T) {
c.Params = Params{Param{}} c.Params = Params{Param{}}
c.Error(errors.New("test")) //nolint: errcheck c.Error(errors.New("test")) //nolint: errcheck
c.Set("foo", "bar") c.Set("foo", "bar")
c.SetSameSite(http.SameSiteLaxMode)
c.reset() c.reset()
assert.False(t, c.IsAborted()) assert.False(t, c.IsAborted())
@ -215,6 +216,7 @@ func TestContextReset(t *testing.T) {
assert.Empty(t, c.Errors.Errors()) assert.Empty(t, c.Errors.Errors())
assert.Empty(t, c.Errors.ByType(ErrorTypeAny)) assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
assert.Empty(t, c.Params) assert.Empty(t, c.Params)
assert.Empty(t, c.cookieOptions)
assert.EqualValues(t, c.index, -1) assert.EqualValues(t, c.index, -1)
assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
} }
@ -866,6 +868,18 @@ func TestContextSetCookie(t *testing.T) {
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie")) assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextSetCookieWithOptions(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true, WithSameSiteCookie(http.SameSiteLaxMode), WithPartitionedCookie(true))
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax; Partitioned", c.Writer.Header().Get("Set-Cookie"))
}
func TestContextSetCookieWithNonSecurePartition(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", false, true, WithPartitionedCookie(true))
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Partitioned", c.Writer.Header().Get("Set-Cookie"))
}
func TestContextSetCookiePathEmpty(t *testing.T) { func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode) c.SetSameSite(http.SameSiteLaxMode)