]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: add ParseCookie, ParseSetCookie
authorJes Cok <xigua67damn@gmail.com>
Thu, 11 Apr 2024 17:52:37 +0000 (01:52 +0800)
committerGopher Robot <gobot@golang.org>
Wed, 17 Apr 2024 17:43:50 +0000 (17:43 +0000)
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>
api/next/66008.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/net/http/66008.md [new file with mode: 0644]
src/net/http/cookie.go
src/net/http/cookie_test.go

diff --git a/api/next/66008.txt b/api/next/66008.txt
new file mode 100644 (file)
index 0000000..ea72f64
--- /dev/null
@@ -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 (file)
index 0000000..e860370
--- /dev/null
@@ -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.
index c22897f3f99e4d238f5ca74ee00e8c5ddd95985d..ab84625ba0bdb9d49c22e3a2782dc59e10da1aee 100644 (file)
@@ -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
 }
index e5bd46a744fba57e9ddce0e637d34cfc33f2f9a1..5337c33aa90a28ee541c73d555026e2182f22a1f 100644 (file)
@@ -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))
+               }
+       }
+}