aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJes Cok <xigua67damn@gmail.com>2024-04-12 01:52:37 +0800
committerGopher Robot <gobot@golang.org>2024-04-17 17:43:50 +0000
commit076166ab4e13506c90448b5d6e0f34c3939ee76f (patch)
treec9b5ed7026e9df8069892c6cd6ef918ad46123c7
parentf367fea83a95c0845ed4733ab1d6ada9ba3087c7 (diff)
downloadgo-076166ab4e13506c90448b5d6e0f34c3939ee76f.tar.gz
go-076166ab4e13506c90448b5d6e0f34c3939ee76f.zip
net/http: add ParseCookie, ParseSetCookie
Fixes #66008 Change-Id: I64acb7da47a03bdef955f394682004906245a18b Reviewed-on: https://go-review.googlesource.com/c/go/+/578275 Reviewed-by: Damien Neil <dneil@google.com> Auto-Submit: Cherry Mui <cherryyz@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Cherry Mui <cherryyz@google.com>
-rw-r--r--api/next/66008.txt2
-rw-r--r--doc/next/6-stdlib/99-minor/net/http/66008.md7
-rw-r--r--src/net/http/cookie.go218
-rw-r--r--src/net/http/cookie_test.go203
4 files changed, 342 insertions, 88 deletions
diff --git a/api/next/66008.txt b/api/next/66008.txt
new file mode 100644
index 0000000000..ea72f64068
--- /dev/null
+++ b/api/next/66008.txt
@@ -0,0 +1,2 @@
+pkg net/http, func ParseCookie(string) ([]*Cookie, error) #66008
+pkg net/http, func ParseSetCookie(string) (*Cookie, error) #66008
diff --git a/doc/next/6-stdlib/99-minor/net/http/66008.md b/doc/next/6-stdlib/99-minor/net/http/66008.md
new file mode 100644
index 0000000000..e8603707ef
--- /dev/null
+++ b/doc/next/6-stdlib/99-minor/net/http/66008.md
@@ -0,0 +1,7 @@
+The new [ParseCookie] function parses a Cookie header value and
+returns all the cookies which were set in it. Since the same cookie
+name can appear multiple times the returned Values can contain
+more than one value for a given key.
+
+The new [ParseSetCookie] function parses a Set-Cookie header value and
+returns a cookie. It returns an error on syntax error.
diff --git a/src/net/http/cookie.go b/src/net/http/cookie.go
index c22897f3f9..ab84625ba0 100644
--- a/src/net/http/cookie.go
+++ b/src/net/http/cookie.go
@@ -55,110 +55,152 @@ const (
SameSiteNoneMode
)
-// readSetCookies parses all "Set-Cookie" values from
-// the header h and returns the successfully parsed Cookies.
-func readSetCookies(h Header) []*Cookie {
- cookieCount := len(h["Set-Cookie"])
- if cookieCount == 0 {
- return []*Cookie{}
- }
- cookies := make([]*Cookie, 0, cookieCount)
- for _, line := range h["Set-Cookie"] {
- parts := strings.Split(textproto.TrimString(line), ";")
- if len(parts) == 1 && parts[0] == "" {
- continue
+var (
+ errBlankCookie = errors.New("http: blank cookie")
+ errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
+ errInvalidCookieName = errors.New("http: invalid cookie name")
+ errInvalidCookieValue = errors.New("http: invalid cookie value")
+)
+
+// ParseCookie parses a Cookie header value and returns all the cookies
+// which were set in it. Since the same cookie name can appear multiple times
+// the returned Values can contain more than one value for a given key.
+func ParseCookie(line string) ([]*Cookie, error) {
+ parts := strings.Split(textproto.TrimString(line), ";")
+ if len(parts) == 1 && parts[0] == "" {
+ return nil, errBlankCookie
+ }
+ cookies := make([]*Cookie, 0, len(parts))
+ for _, s := range parts {
+ s = textproto.TrimString(s)
+ name, value, found := strings.Cut(s, "=")
+ if !found {
+ return nil, errEqualNotFoundInCookie
}
- parts[0] = textproto.TrimString(parts[0])
- name, value, ok := strings.Cut(parts[0], "=")
- if !ok {
+ if !isCookieNameValid(name) {
+ return nil, errInvalidCookieName
+ }
+ value, found = parseCookieValue(value, true)
+ if !found {
+ return nil, errInvalidCookieValue
+ }
+ cookies = append(cookies, &Cookie{Name: name, Value: value})
+ }
+ return cookies, nil
+}
+
+// ParseSetCookie parses a Set-Cookie header value and returns a cookie.
+// It returns an error on syntax error.
+func ParseSetCookie(line string) (*Cookie, error) {
+ parts := strings.Split(textproto.TrimString(line), ";")
+ if len(parts) == 1 && parts[0] == "" {
+ return nil, errBlankCookie
+ }
+ parts[0] = textproto.TrimString(parts[0])
+ name, value, ok := strings.Cut(parts[0], "=")
+ if !ok {
+ return nil, errEqualNotFoundInCookie
+ }
+ name = textproto.TrimString(name)
+ if !isCookieNameValid(name) {
+ return nil, errInvalidCookieName
+ }
+ value, ok = parseCookieValue(value, true)
+ if !ok {
+ return nil, errInvalidCookieValue
+ }
+ c := &Cookie{
+ Name: name,
+ Value: value,
+ Raw: line,
+ }
+ for i := 1; i < len(parts); i++ {
+ parts[i] = textproto.TrimString(parts[i])
+ if len(parts[i]) == 0 {
continue
}
- name = textproto.TrimString(name)
- if !isCookieNameValid(name) {
+
+ attr, val, _ := strings.Cut(parts[i], "=")
+ lowerAttr, isASCII := ascii.ToLower(attr)
+ if !isASCII {
continue
}
- value, ok = parseCookieValue(value, true)
+ val, ok = parseCookieValue(val, false)
if !ok {
+ c.Unparsed = append(c.Unparsed, parts[i])
continue
}
- c := &Cookie{
- Name: name,
- Value: value,
- Raw: line,
- }
- for i := 1; i < len(parts); i++ {
- parts[i] = textproto.TrimString(parts[i])
- if len(parts[i]) == 0 {
- continue
- }
- attr, val, _ := strings.Cut(parts[i], "=")
- lowerAttr, isASCII := ascii.ToLower(attr)
- if !isASCII {
+ switch lowerAttr {
+ case "samesite":
+ lowerVal, ascii := ascii.ToLower(val)
+ if !ascii {
+ c.SameSite = SameSiteDefaultMode
continue
}
- val, ok = parseCookieValue(val, false)
- if !ok {
- c.Unparsed = append(c.Unparsed, parts[i])
- continue
+ switch lowerVal {
+ case "lax":
+ c.SameSite = SameSiteLaxMode
+ case "strict":
+ c.SameSite = SameSiteStrictMode
+ case "none":
+ c.SameSite = SameSiteNoneMode
+ default:
+ c.SameSite = SameSiteDefaultMode
}
-
- switch lowerAttr {
- case "samesite":
- lowerVal, ascii := ascii.ToLower(val)
- if !ascii {
- c.SameSite = SameSiteDefaultMode
- continue
- }
- switch lowerVal {
- case "lax":
- c.SameSite = SameSiteLaxMode
- case "strict":
- c.SameSite = SameSiteStrictMode
- case "none":
- c.SameSite = SameSiteNoneMode
- default:
- c.SameSite = SameSiteDefaultMode
- }
- continue
- case "secure":
- c.Secure = true
- continue
- case "httponly":
- c.HttpOnly = true
- continue
- case "domain":
- c.Domain = val
- continue
- case "max-age":
- secs, err := strconv.Atoi(val)
- if err != nil || secs != 0 && val[0] == '0' {
- break
- }
- if secs <= 0 {
- secs = -1
- }
- c.MaxAge = secs
- continue
- case "expires":
- c.RawExpires = val
- exptime, err := time.Parse(time.RFC1123, val)
+ continue
+ case "secure":
+ c.Secure = true
+ continue
+ case "httponly":
+ c.HttpOnly = true
+ continue
+ case "domain":
+ c.Domain = val
+ continue
+ case "max-age":
+ secs, err := strconv.Atoi(val)
+ if err != nil || secs != 0 && val[0] == '0' {
+ break
+ }
+ if secs <= 0 {
+ secs = -1
+ }
+ c.MaxAge = secs
+ continue
+ case "expires":
+ c.RawExpires = val
+ exptime, err := time.Parse(time.RFC1123, val)
+ if err != nil {
+ exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
if err != nil {
- exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
- if err != nil {
- c.Expires = time.Time{}
- break
- }
+ c.Expires = time.Time{}
+ break
}
- c.Expires = exptime.UTC()
- continue
- case "path":
- c.Path = val
- continue
}
- c.Unparsed = append(c.Unparsed, parts[i])
+ c.Expires = exptime.UTC()
+ continue
+ case "path":
+ c.Path = val
+ continue
+ }
+ c.Unparsed = append(c.Unparsed, parts[i])
+ }
+ return c, nil
+}
+
+// readSetCookies parses all "Set-Cookie" values from
+// the header h and returns the successfully parsed Cookies.
+func readSetCookies(h Header) []*Cookie {
+ cookieCount := len(h["Set-Cookie"])
+ if cookieCount == 0 {
+ return []*Cookie{}
+ }
+ cookies := make([]*Cookie, 0, cookieCount)
+ for _, line := range h["Set-Cookie"] {
+ if cookie, err := ParseSetCookie(line); err == nil {
+ cookies = append(cookies, cookie)
}
- cookies = append(cookies, c)
}
return cookies
}
diff --git a/src/net/http/cookie_test.go b/src/net/http/cookie_test.go
index e5bd46a744..5337c33aa9 100644
--- a/src/net/http/cookie_test.go
+++ b/src/net/http/cookie_test.go
@@ -6,6 +6,7 @@ package http
import (
"encoding/json"
+ "errors"
"fmt"
"log"
"os"
@@ -650,3 +651,205 @@ func BenchmarkReadCookies(b *testing.B) {
b.Fatalf("readCookies:\nhave: %s\nwant: %s\n", toJSON(c), toJSON(wantCookies))
}
}
+
+func TestParseCookie(t *testing.T) {
+ tests := []struct {
+ line string
+ cookies []*Cookie
+ err error
+ }{
+ {
+ line: "Cookie-1=v$1",
+ cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}},
+ },
+ {
+ line: "Cookie-1=v$1;c2=v2",
+ cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}, {Name: "c2", Value: "v2"}},
+ },
+ {
+ line: `Cookie-1="v$1";c2="v2"`,
+ cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}, {Name: "c2", Value: "v2"}},
+ },
+ {
+ line: "k1=",
+ cookies: []*Cookie{{Name: "k1", Value: ""}},
+ },
+ {
+ line: "",
+ err: errBlankCookie,
+ },
+ {
+ line: "whatever",
+ err: errEqualNotFoundInCookie,
+ },
+ {
+ line: "=v1",
+ err: errInvalidCookieName,
+ },
+ {
+ line: "k1=\\",
+ err: errInvalidCookieValue,
+ },
+ }
+ for i, tt := range tests {
+ gotCookies, gotErr := ParseCookie(tt.line)
+ if !errors.Is(gotErr, tt.err) {
+ t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err)
+ }
+ if !reflect.DeepEqual(gotCookies, tt.cookies) {
+ t.Errorf("#%d ParseCookie:\ngot cookies: %s\nwant cookies: %s\n", i, toJSON(gotCookies), toJSON(tt.cookies))
+ }
+ }
+}
+
+func TestParseSetCookie(t *testing.T) {
+ tests := []struct {
+ line string
+ cookie *Cookie
+ err error
+ }{
+ {
+ line: "Cookie-1=v$1",
+ cookie: &Cookie{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"},
+ },
+ {
+ line: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly",
+ cookie: &Cookie{
+ Name: "NID",
+ Value: "99=YsDT5i3E-CXax-",
+ Path: "/",
+ Domain: ".google.ch",
+ HttpOnly: true,
+ Expires: time.Date(2011, 11, 23, 1, 5, 3, 0, time.UTC),
+ RawExpires: "Wed, 23-Nov-2011 01:05:03 GMT",
+ Raw: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly",
+ },
+ },
+ {
+ line: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly",
+ cookie: &Cookie{
+ Name: ".ASPXAUTH",
+ Value: "7E3AA",
+ Path: "/",
+ Expires: time.Date(2012, 3, 7, 14, 25, 6, 0, time.UTC),
+ RawExpires: "Wed, 07-Mar-2012 14:25:06 GMT",
+ HttpOnly: true,
+ Raw: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly",
+ },
+ },
+ {
+ line: "ASP.NET_SessionId=foo; path=/; HttpOnly",
+ cookie: &Cookie{
+ Name: "ASP.NET_SessionId",
+ Value: "foo",
+ Path: "/",
+ HttpOnly: true,
+ Raw: "ASP.NET_SessionId=foo; path=/; HttpOnly",
+ },
+ },
+ {
+ line: "samesitedefault=foo; SameSite",
+ cookie: &Cookie{
+ Name: "samesitedefault",
+ Value: "foo",
+ SameSite: SameSiteDefaultMode,
+ Raw: "samesitedefault=foo; SameSite",
+ },
+ },
+ {
+ line: "samesiteinvalidisdefault=foo; SameSite=invalid",
+ cookie: &Cookie{
+ Name: "samesiteinvalidisdefault",
+ Value: "foo",
+ SameSite: SameSiteDefaultMode,
+ Raw: "samesiteinvalidisdefault=foo; SameSite=invalid",
+ },
+ },
+ {
+ line: "samesitelax=foo; SameSite=Lax",
+ cookie: &Cookie{
+ Name: "samesitelax",
+ Value: "foo",
+ SameSite: SameSiteLaxMode,
+ Raw: "samesitelax=foo; SameSite=Lax",
+ },
+ },
+ {
+ line: "samesitestrict=foo; SameSite=Strict",
+ cookie: &Cookie{
+ Name: "samesitestrict",
+ Value: "foo",
+ SameSite: SameSiteStrictMode,
+ Raw: "samesitestrict=foo; SameSite=Strict",
+ },
+ },
+ {
+ line: "samesitenone=foo; SameSite=None",
+ cookie: &Cookie{
+ Name: "samesitenone",
+ Value: "foo",
+ SameSite: SameSiteNoneMode,
+ Raw: "samesitenone=foo; SameSite=None",
+ },
+ },
+ // Make sure we can properly read back the Set-Cookie headers we create
+ // for values containing spaces or commas:
+ {
+ line: `special-1=a z`,
+ cookie: &Cookie{Name: "special-1", Value: "a z", Raw: `special-1=a z`},
+ },
+ {
+ line: `special-2=" z"`,
+ cookie: &Cookie{Name: "special-2", Value: " z", Raw: `special-2=" z"`},
+ },
+ {
+ line: `special-3="a "`,
+ cookie: &Cookie{Name: "special-3", Value: "a ", Raw: `special-3="a "`},
+ },
+ {
+ line: `special-4=" "`,
+ cookie: &Cookie{Name: "special-4", Value: " ", Raw: `special-4=" "`},
+ },
+ {
+ line: `special-5=a,z`,
+ cookie: &Cookie{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`},
+ },
+ {
+ line: `special-6=",z"`,
+ cookie: &Cookie{Name: "special-6", Value: ",z", Raw: `special-6=",z"`},
+ },
+ {
+ line: `special-7=a,`,
+ cookie: &Cookie{Name: "special-7", Value: "a,", Raw: `special-7=a,`},
+ },
+ {
+ line: `special-8=","`,
+ cookie: &Cookie{Name: "special-8", Value: ",", Raw: `special-8=","`},
+ },
+ {
+ line: "",
+ err: errBlankCookie,
+ },
+ {
+ line: "whatever",
+ err: errEqualNotFoundInCookie,
+ },
+ {
+ line: "=v1",
+ err: errInvalidCookieName,
+ },
+ {
+ line: "k1=\\",
+ err: errInvalidCookieValue,
+ },
+ }
+ for i, tt := range tests {
+ gotCookie, gotErr := ParseSetCookie(tt.line)
+ if !errors.Is(gotErr, tt.err) {
+ t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err)
+ }
+ if !reflect.DeepEqual(gotCookie, tt.cookie) {
+ t.Errorf("#%d ParseCookie:\ngot cookie: %s\nwant cookie: %s\n", i, toJSON(gotCookie), toJSON(tt.cookie))
+ }
+ }
+}