Repository: cloudwego/hertz
Pull request #1041 (2024-01-08 17:02:48)

feat: support partitioned cookies
#### What type of PR is this?

Add support for partitioned cookie.

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.
- [x] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.

feat(cookie): 支持独立分区 Cookies (CHIPS)
#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en:

Chrome has started restricting third-party cookies by default for 1% of Chrome browsers, which means third-party cookies with the SameSite=None attribute will be blocked. [Partitioned cookies](https://developers.google.com/privacy-sandbox/3pcd/chips) has been introduced as a privacy preserving alternative. A partitioned cookie is tied to the top-level site where it's initially set and cannot be accessed from elsewhere. 

```
Set-Cookie: __Host-name=value; Secure; Path=/; SameSite=None; Partitioned;
```
![image](https://github.com/cloudwego/hertz/assets/34025121/76daf961-fa4f-4466-a17c-234411dedab9)
![image](https://github.com/cloudwego/hertz/assets/34025121/9d8a2e03-af43-4419-8b09-6f16e2cf2080)


You can learn more about CHIPS [here](https://developers.google.com/privacy-sandbox/3pcd/chips) or try the [demo](https://chips-site-a.glitch.me/).


The net/http library has adopted partitioned cookies with https://github.com/golang/go/issues/62490 and https://github.com/cloudwego/hertz/issues/1032. 

To support partitioned cookies for Hertz, the following changes have been made:
- Add `partitioned` field to Cookie
- Append `Partitioned;` to the end of the cookie if partitioned is set to true.
- Add `Partitioned` and `SetPartitioned` to get and set partitioned. SetPartitioned to true, will also set Secure.
- Set partitioned to false when cookie is reset.

zh(optional):

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

Fixes https://github.com/cloudwego/hertz/issues/1032

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->
No

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/cloudwego/hertz/pull/1041?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`fc0abf1`)](https://app.codecov.io/gh/cloudwego/hertz/commit/fc0abf18038eab49359b4abf00bc50dee1ea3bf2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 82.54% compared to head [(`192ef86`)](https://app.codecov.io/gh/cloudwego/hertz/pull/1041?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 82.60%.
> Report is 1 commits behind head on develop.


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #1041      +/-   ##
===========================================
+ Coverage    82.54%   82.60%   +0.05%     
===========================================
  Files           98       98              
  Lines         9982     9999      +17     
===========================================
+ Hits          8240     8260      +20     
+ Misses        1244     1241       -3     
  Partials       498      498              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cloudwego/hertz/pull/1041?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).


Patch:
diff --git a/pkg/common/adaptor/response.go b/pkg/common/adaptor/response.go
index e981007..4dbc0fa 100644
--- a/pkg/common/adaptor/response.go
+++ b/pkg/common/adaptor/response.go
@@ -54,7 +54,7 @@ func (c *compatResponse) WriteHeader(statusCode int) {
 				}
 				if k == consts.HeaderSetCookie {
 					cookie := protocol.AcquireCookie()
-					cookie.Parse(vv)
+					_ = cookie.Parse(vv)
 					c.h.Header.SetCookie(cookie)
 					continue
 				}
diff --git a/pkg/protocol/cookie.go b/pkg/protocol/cookie.go
index 7ca22c6..4477a89 100644
--- a/pkg/protocol/cookie.go
+++ b/pkg/protocol/cookie.go
@@ -65,6 +65,8 @@ const (
 	CookieSameSiteStrictMode
 	// CookieSameSiteNoneMode sets the SameSite flag with the "None" parameter
 	// see https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
+	// third-party cookies are phasing out, use Partitioned cookies instead
+	// see https://developers.google.com/privacy-sandbox/3pcd
 	CookieSameSiteNoneMode
 )
 
@@ -101,7 +103,10 @@ type Cookie struct {
 
 	httpOnly bool
 	secure   bool
-	sameSite CookieSameSite
+	// A partitioned third-party cookie is tied to the top-level site
+	// where it's initially set and cannot be accessed from elsewhere.
+	partitioned bool
+	sameSite    CookieSameSite
 
 	bufKV argsKV
 	buf   []byte
@@ -222,6 +227,12 @@ func (c *Cookie) AppendBytes(dst []byte) []byte {
 	case CookieSameSiteNoneMode:
 		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteNone)
 	}
+
+	if c.partitioned {
+		dst = append(dst, ';', ' ')
+		dst = append(dst, bytestr.StrCookiePartitioned...)
+	}
+
 	return dst
 }
 
@@ -337,6 +348,7 @@ func (c *Cookie) Reset() {
 	c.httpOnly = false
 	c.secure = false
 	c.sameSite = CookieSameSiteDisabled
+	c.partitioned = false
 }
 
 // Value returns cookie value.
@@ -438,9 +450,14 @@ func (c *Cookie) ParseBytes(src []byte) error {
 				} else if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSite, kv.value) {
 					c.sameSite = CookieSameSiteDefaultMode
 				}
+			case 'p': // "partitioned"
+				if utils.CaseInsensitiveCompare(bytestr.StrCookiePartitioned, kv.value) {
+					c.partitioned = true
+				}
 			}
 		} // else empty or no match
 	}
+
 	return nil
 }
 
@@ -496,6 +513,11 @@ func (c *Cookie) SameSite() CookieSameSite {
 	return c.sameSite
 }
 
+// Partitioned returns if cookie is partitioned.
+func (c *Cookie) Partitioned() bool {
+	return c.partitioned
+}
+
 // SetSameSite sets the cookie's SameSite flag to the given value.
 // set value CookieSameSiteNoneMode will set Secure to true also to avoid browser rejection
 func (c *Cookie) SetSameSite(mode CookieSameSite) {
@@ -515,6 +537,14 @@ func (c *Cookie) SetHTTPOnly(httpOnly bool) {
 	c.httpOnly = httpOnly
 }
 
+// SetPartitioned sets cookie as partitioned. Setting Partitioned to true will also set Secure.
+func (c *Cookie) SetPartitioned(partitioned bool) {
+	c.partitioned = partitioned
+	if partitioned {
+		c.SetSecure(true)
+	}
+}
+
 // String returns cookie representation.
 func (c *Cookie) String() string {
 	return string(c.Cookie())
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index d53f12a..278b137 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -1298,7 +1298,6 @@ func (h *RequestHeader) SetCookie(key, value string) {
 }
 
 // SetCookie sets the given response cookie.
-//
 // It is save re-using the cookie after the function returns.
 func (h *ResponseHeader) SetCookie(cookie *Cookie) {
 	h.cookies = setArgBytes(h.cookies, cookie.Key(), cookie.Cookie(), ArgsHasValue)


Test patch:
diff --git a/internal/bytestr/bytes.go b/internal/bytestr/bytes.go
index a93bd2d..bbf3dcb 100644
--- a/internal/bytestr/bytes.go
+++ b/internal/bytestr/bytes.go
@@ -97,6 +97,7 @@ var (
 	StrCookieSameSiteLax    = []byte("Lax")
 	StrCookieSameSiteStrict = []byte("Strict")
 	StrCookieSameSiteNone   = []byte("None")
+	StrCookiePartitioned    = []byte("Partitioned")
 
 	StrClose               = []byte("close")
 	StrGzip                = []byte("gzip")
diff --git a/pkg/protocol/cookie_test.go b/pkg/protocol/cookie_test.go
index f7637e5..d799441 100644
--- a/pkg/protocol/cookie_test.go
+++ b/pkg/protocol/cookie_test.go
@@ -67,6 +67,9 @@ func TestCookieAppendBytes(t *testing.T) {
 
 	c.SetExpire(CookieExpireDelete)
 	testCookieAppendBytes(t, c, "xxx", "yyy", "xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b")
+
+	c.SetPartitioned(true)
+	testCookieAppendBytes(t, c, "xxx", "yyy", "xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b; secure; Partitioned")
 }
 
 func testCookieAppendBytes(t *testing.T, c *Cookie, key, value, expectedS string) {
@@ -257,6 +260,31 @@ func TestCookieSameSite(t *testing.T) {
 	}
 }
 
+func TestCookiePartitioned(t *testing.T) {
+	t.Parallel()
+
+	var c Cookie
+
+	if err := c.Parse("__Host-name=value; Secure; Path=/; SameSite=None; Partitioned;"); err != nil {
+		t.Fatalf("unexpected error for valid paritionedd cookie: %s", err)
+	}
+	if !c.Partitioned() {
+		t.Fatalf("partitioned must be set")
+	}
+
+	if err := c.Parse("foo=bar"); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	c.SetPartitioned(true)
+	s := c.String()
+	if !strings.Contains(s, "; Partitioned") {
+		t.Fatalf("missing Partitioned flag in cookie %q", s)
+	}
+	if !strings.Contains(s, "; secure") {
+		t.Fatalf("missing Secure flag in cookie %q", s)
+	}
+}
+
 func TestCookieMaxAge(t *testing.T) {
 	t.Parallel()