Skip to content

Commit 192ef86

Browse files
committed
feat: Parse and AppendBytes for Partitioned Cookie
1 parent fc0abf1 commit 192ef86

File tree

5 files changed

+83
-3
lines changed

5 files changed

+83
-3
lines changed

internal/bytestr/bytes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ var (
9797
StrCookieSameSiteLax = []byte("Lax")
9898
StrCookieSameSiteStrict = []byte("Strict")
9999
StrCookieSameSiteNone = []byte("None")
100+
StrCookiePartitioned = []byte("Partitioned")
100101

101102
StrClose = []byte("close")
102103
StrGzip = []byte("gzip")

pkg/common/adaptor/response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (c *compatResponse) WriteHeader(statusCode int) {
5454
}
5555
if k == consts.HeaderSetCookie {
5656
cookie := protocol.AcquireCookie()
57-
cookie.Parse(vv)
57+
_ = cookie.Parse(vv)
5858
c.h.Header.SetCookie(cookie)
5959
continue
6060
}

pkg/protocol/cookie.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const (
6565
CookieSameSiteStrictMode
6666
// CookieSameSiteNoneMode sets the SameSite flag with the "None" parameter
6767
// see https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
68+
// third-party cookies are phasing out, use Partitioned cookies instead
69+
// see https://developers.google.com/privacy-sandbox/3pcd
6870
CookieSameSiteNoneMode
6971
)
7072

@@ -73,6 +75,8 @@ var zeroTime time.Time
7375
var (
7476
errNoCookies = errors.NewPublic("no cookies found")
7577

78+
errInvalidPartitionedCookie = errors.NewPublic("http: partitioned cookies must be set with Secure and Path=/")
79+
7680
// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
7781
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
7882

@@ -101,7 +105,10 @@ type Cookie struct {
101105

102106
httpOnly bool
103107
secure bool
104-
sameSite CookieSameSite
108+
// A partitioned third-party cookie is tied to the top-level site
109+
// where it's initially set and cannot be accessed from elsewhere.
110+
partitioned bool
111+
sameSite CookieSameSite
105112

106113
bufKV argsKV
107114
buf []byte
@@ -222,6 +229,12 @@ func (c *Cookie) AppendBytes(dst []byte) []byte {
222229
case CookieSameSiteNoneMode:
223230
dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteNone)
224231
}
232+
233+
if c.partitioned {
234+
dst = append(dst, ';', ' ')
235+
dst = append(dst, bytestr.StrCookiePartitioned...)
236+
}
237+
225238
return dst
226239
}
227240

@@ -337,6 +350,7 @@ func (c *Cookie) Reset() {
337350
c.httpOnly = false
338351
c.secure = false
339352
c.sameSite = CookieSameSiteDisabled
353+
c.partitioned = false
340354
}
341355

342356
// Value returns cookie value.
@@ -438,9 +452,21 @@ func (c *Cookie) ParseBytes(src []byte) error {
438452
} else if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSite, kv.value) {
439453
c.sameSite = CookieSameSiteDefaultMode
440454
}
455+
case 'p': // "partitioned"
456+
if utils.CaseInsensitiveCompare(bytestr.StrCookiePartitioned, kv.value) {
457+
c.partitioned = true
458+
}
441459
}
442460
} // else empty or no match
443461
}
462+
463+
if c.partitioned {
464+
// Partitioned cookies must be set with Secure.
465+
if !c.secure || !utils.CaseInsensitiveCompare(c.path, bytestr.StrSlash) {
466+
return errInvalidPartitionedCookie
467+
}
468+
}
469+
444470
return nil
445471
}
446472

@@ -496,6 +522,11 @@ func (c *Cookie) SameSite() CookieSameSite {
496522
return c.sameSite
497523
}
498524

525+
// Partitioned returns if cookie is partitioned.
526+
func (c *Cookie) Partitioned() bool {
527+
return c.partitioned
528+
}
529+
499530
// SetSameSite sets the cookie's SameSite flag to the given value.
500531
// set value CookieSameSiteNoneMode will set Secure to true also to avoid browser rejection
501532
func (c *Cookie) SetSameSite(mode CookieSameSite) {
@@ -515,6 +546,15 @@ func (c *Cookie) SetHTTPOnly(httpOnly bool) {
515546
c.httpOnly = httpOnly
516547
}
517548

549+
// SetPartitioned sets cookie as partitioned. Set Partitioned to true will also set Secure and Path to "/".
550+
func (c *Cookie) SetPartitioned(partitioned bool) {
551+
c.partitioned = partitioned
552+
if partitioned {
553+
c.SetSecure(true)
554+
c.SetPathBytes(bytestr.StrSlash)
555+
}
556+
}
557+
518558
// String returns cookie representation.
519559
func (c *Cookie) String() string {
520560
return string(c.Cookie())

pkg/protocol/cookie_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
package protocol
4343

4444
import (
45+
"errors"
4546
"math/rand"
4647
"strings"
4748
"testing"
@@ -67,6 +68,9 @@ func TestCookieAppendBytes(t *testing.T) {
6768

6869
c.SetExpire(CookieExpireDelete)
6970
testCookieAppendBytes(t, c, "xxx", "yyy", "xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b")
71+
72+
c.SetPartitioned(true)
73+
testCookieAppendBytes(t, c, "xxx", "yyy", "xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/; secure; Partitioned")
7074
}
7175

7276
func testCookieAppendBytes(t *testing.T, c *Cookie, key, value, expectedS string) {
@@ -257,6 +261,42 @@ func TestCookieSameSite(t *testing.T) {
257261
}
258262
}
259263

264+
func TestCookiePartitioned(t *testing.T) {
265+
t.Parallel()
266+
267+
var c Cookie
268+
269+
if err := c.Parse("__Host-name=value; Secure; Path=/; SameSite=None; Partitioned;"); err != nil {
270+
t.Fatalf("unexpected error for valid paritionedd cookie: %s", err)
271+
}
272+
if !c.Partitioned() {
273+
t.Fatalf("partitioned must be set")
274+
}
275+
276+
if err := c.Parse("__Host-name=value; Path=/; SameSite=None; Partitioned;"); !errors.Is(err, errInvalidPartitionedCookie) {
277+
t.Fatalf("invalid secure for partitioned cookie")
278+
}
279+
280+
if err := c.Parse("__Host-name=value; Secure; SameSite=None; Partitioned;"); !errors.Is(err, errInvalidPartitionedCookie) {
281+
t.Fatalf("invalid path for partitioned cookie")
282+
}
283+
284+
if err := c.Parse("foo=bar"); err != nil {
285+
t.Fatalf("unexpected error: %s", err)
286+
}
287+
c.SetPartitioned(true)
288+
s := c.String()
289+
if !strings.Contains(s, "; Partitioned") {
290+
t.Fatalf("missing Partitioned flag in cookie %q", s)
291+
}
292+
if !strings.Contains(s, "; secure") {
293+
t.Fatalf("missing Secure flag in cookie %q", s)
294+
}
295+
if !strings.Contains(s, "; path=/") {
296+
t.Fatalf("path is not top-level site in cookie %q", s)
297+
}
298+
}
299+
260300
func TestCookieMaxAge(t *testing.T) {
261301
t.Parallel()
262302

pkg/protocol/header.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,6 @@ func (h *RequestHeader) SetCookie(key, value string) {
12981298
}
12991299

13001300
// SetCookie sets the given response cookie.
1301-
//
13021301
// It is save re-using the cookie after the function returns.
13031302
func (h *ResponseHeader) SetCookie(cookie *Cookie) {
13041303
h.cookies = setArgBytes(h.cookies, cookie.Key(), cookie.Cookie(), ArgsHasValue)

0 commit comments

Comments
 (0)