mirror of
https://github.com/golang/go
synced 2024-11-22 10:44:41 -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:
parent
f367fea83a
commit
076166ab4e
2
api/next/66008.txt
Normal file
2
api/next/66008.txt
Normal file
@ -0,0 +1,2 @@
|
||||
pkg net/http, func ParseCookie(string) ([]*Cookie, error) #66008
|
||||
pkg net/http, func ParseSetCookie(string) (*Cookie, error) #66008
|
7
doc/next/6-stdlib/99-minor/net/http/66008.md
Normal file
7
doc/next/6-stdlib/99-minor/net/http/66008.md
Normal 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.
|
@ -55,31 +55,59 @@ 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"] {
|
||||
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] == "" {
|
||||
continue
|
||||
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 {
|
||||
continue
|
||||
return nil, errEqualNotFoundInCookie
|
||||
}
|
||||
name = textproto.TrimString(name)
|
||||
if !isCookieNameValid(name) {
|
||||
continue
|
||||
return nil, errInvalidCookieName
|
||||
}
|
||||
value, ok = parseCookieValue(value, true)
|
||||
if !ok {
|
||||
continue
|
||||
return nil, errInvalidCookieValue
|
||||
}
|
||||
c := &Cookie{
|
||||
Name: name,
|
||||
@ -158,7 +186,21 @@ func readSetCookies(h Header) []*Cookie {
|
||||
}
|
||||
c.Unparsed = append(c.Unparsed, parts[i])
|
||||
}
|
||||
cookies = append(cookies, c)
|
||||
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)
|
||||
}
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user