implement partitioned cookies

implement cookie validation with error logging
refactor c.SetCookie to accept option pattern modifications as vararg
refactor c.SetSameSite to use option pattern under the hood
This commit is contained in:
bound2 2025-03-28 09:09:45 +02:00
parent 8763f33c65
commit 4eecebf8bf
2 changed files with 51 additions and 9 deletions

View File

@ -6,9 +6,11 @@ package gin
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"math"
"mime/multipart"
"net"
@ -47,6 +49,20 @@ const ContextKey = "_gin-gonic/gin/contextkey"
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
// abortIndex represents a typical value used in abort functions.
@ -87,9 +103,9 @@ type Context struct {
// or PUT body parameters.
formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
// cookieOptions set up additional cookie parameters to be used with http.Cookie
// this is needed to prevent breaking existing implementations
cookieOptions []CookieOption
}
/************************************/
@ -108,7 +124,7 @@ func (c *Context) reset() {
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
c.sameSite = 0
c.cookieOptions = make([]CookieOption, 0)
*c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0]
}
@ -1005,26 +1021,38 @@ func (c *Context) GetRawData() ([]byte, error) {
// SetSameSite with cookie
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.
// 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, options ...CookieOption) {
if path == "" {
path = "/"
}
http.SetCookie(c.Writer, &http.Cookie{
cookie := &http.Cookie{
Name: name,
Value: url.QueryEscape(value),
MaxAge: maxAge,
Path: path,
Domain: domain,
SameSite: c.sameSite,
Secure: secure,
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

View File

@ -206,6 +206,7 @@ func TestContextReset(t *testing.T) {
c.Params = Params{Param{}}
c.Error(errors.New("test")) //nolint: errcheck
c.Set("foo", "bar")
c.SetSameSite(http.SameSiteLaxMode)
c.reset()
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.ByType(ErrorTypeAny))
assert.Empty(t, c.Params)
assert.Empty(t, c.cookieOptions)
assert.EqualValues(t, c.index, -1)
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"))
}
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) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)