1
0
mirror of https://github.com/golang/go synced 2024-11-22 04:54:42 -07:00

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>
This commit is contained in:
Jes Cok 2024-04-12 01:52:37 +08:00 committed by Gopher Robot
parent f367fea83a
commit 076166ab4e
4 changed files with 348 additions and 94 deletions

2
api/next/66008.txt Normal file
View File

@ -0,0 +1,2 @@
pkg net/http, func ParseCookie(string) ([]*Cookie, error) #66008
pkg net/http, func ParseSetCookie(string) (*Cookie, error) #66008

View File

@ -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.

View File

@ -55,6 +55,140 @@ const (
SameSiteNoneMode
)
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
}
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
}
attr, val, _ := strings.Cut(parts[i], "=")
lowerAttr, isASCII := ascii.ToLower(attr)
if !isASCII {
continue
}
val, ok = parseCookieValue(val, false)
if !ok {
c.Unparsed = append(c.Unparsed, parts[i])
continue
}
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)
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 = 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 {
@ -64,101 +198,9 @@ func readSetCookies(h Header) []*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
if cookie, err := ParseSetCookie(line); err == nil {
cookies = append(cookies, cookie)
}
parts[0] = textproto.TrimString(parts[0])
name, value, ok := strings.Cut(parts[0], "=")
if !ok {
continue
}
name = textproto.TrimString(name)
if !isCookieNameValid(name) {
continue
}
value, ok = parseCookieValue(value, true)
if !ok {
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 {
continue
}
val, ok = parseCookieValue(val, false)
if !ok {
c.Unparsed = append(c.Unparsed, parts[i])
continue
}
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)
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 = exptime.UTC()
continue
case "path":
c.Path = val
continue
}
c.Unparsed = append(c.Unparsed, parts[i])
}
cookies = append(cookies, c)
}
return cookies
}

View 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))
}
}
}