Skip to content

net/http: add field Cookie.Quoted bool #66752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/next/46443.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg net/http, type Cookie struct, Quoted bool #46443
3 changes: 3 additions & 0 deletions doc/next/6-stdlib/99-minor/net/http/46443.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[`Cookie`](/pkg/net/http#Cookie) now preserves double quotes surrounding
a cookie value. The new `Cookie.Quoted` field indicates whether the
`Cookie.Value` was originally quoted.
51 changes: 32 additions & 19 deletions src/net/http/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import (
//
// See https://tools.ietf.org/html/rfc6265 for details.
type Cookie struct {
Name string
Value string
Name string
Value string
Quoted bool // indicates whether the Value was originally quoted

Path string // optional
Domain string // optional
Expand Down Expand Up @@ -80,11 +81,11 @@ func ParseCookie(line string) ([]*Cookie, error) {
if !isCookieNameValid(name) {
return nil, errInvalidCookieName
}
value, found = parseCookieValue(value, true)
value, quoted, found := parseCookieValue(value, true)
if !found {
return nil, errInvalidCookieValue
}
cookies = append(cookies, &Cookie{Name: name, Value: value})
cookies = append(cookies, &Cookie{Name: name, Value: value, Quoted: quoted})
}
return cookies, nil
}
Expand All @@ -105,14 +106,15 @@ func ParseSetCookie(line string) (*Cookie, error) {
if !isCookieNameValid(name) {
return nil, errInvalidCookieName
}
value, ok = parseCookieValue(value, true)
value, quoted, ok := parseCookieValue(value, true)
if !ok {
return nil, errInvalidCookieValue
}
c := &Cookie{
Name: name,
Value: value,
Raw: line,
Name: name,
Value: value,
Quoted: quoted,
Raw: line,
}
for i := 1; i < len(parts); i++ {
parts[i] = textproto.TrimString(parts[i])
Expand All @@ -125,7 +127,7 @@ func ParseSetCookie(line string) (*Cookie, error) {
if !isASCII {
continue
}
val, ok = parseCookieValue(val, false)
val, _, ok = parseCookieValue(val, false)
if !ok {
c.Unparsed = append(c.Unparsed, parts[i])
continue
Expand Down Expand Up @@ -229,7 +231,7 @@ func (c *Cookie) String() string {
b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
b.WriteString(c.Name)
b.WriteRune('=')
b.WriteString(sanitizeCookieValue(c.Value))
b.WriteString(sanitizeCookieValue(c.Value, c.Quoted))

if len(c.Path) > 0 {
b.WriteString("; Path=")
Expand Down Expand Up @@ -341,11 +343,11 @@ func readCookies(h Header, filter string) []*Cookie {
if filter != "" && filter != name {
continue
}
val, ok := parseCookieValue(val, true)
val, quoted, ok := parseCookieValue(val, true)
if !ok {
continue
}
cookies = append(cookies, &Cookie{Name: name, Value: val})
cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted})
}
}
return cookies
Expand Down Expand Up @@ -430,6 +432,8 @@ func sanitizeCookieName(n string) string {
}

// sanitizeCookieValue produces a suitable cookie-value from v.
// It receives a quoted bool indicating whether the value was originally
// quoted.
// https://tools.ietf.org/html/rfc6265#section-4.1.1
//
// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
Expand All @@ -439,15 +443,14 @@ func sanitizeCookieName(n string) string {
// ; and backslash
//
// We loosen this as spaces and commas are common in cookie values
// but we produce a quoted cookie-value if and only if v contains
// commas or spaces.
// thus we produce a quoted cookie-value if v contains commas or spaces.
// See https://golang.org/issue/7243 for the discussion.
func sanitizeCookieValue(v string) string {
func sanitizeCookieValue(v string, quoted bool) string {
v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
if len(v) == 0 {
return v
}
if strings.ContainsAny(v, " ,") {
if strings.ContainsAny(v, " ,") || quoted {
return `"` + v + `"`
}
return v
Expand Down Expand Up @@ -489,17 +492,27 @@ func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
return string(buf)
}

func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) {
// parseCookieValue parses a cookie value according to RFC 6265.
// If allowDoubleQuote is true, parseCookieValue will consider that it
// is parsing the cookie-value;
// otherwise, it will consider that it is parsing a cookie-av value
// (cookie attribute-value).
//
// It returns the parsed cookie value, a boolean indicating whether the
// parsing was successful, and a boolean indicating whether the parsed
// value was enclosed in double quotes.
func parseCookieValue(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) {
// Strip the quotes, if present.
if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
raw = raw[1 : len(raw)-1]
quoted = true
}
for i := 0; i < len(raw); i++ {
if !validCookieValueByte(raw[i]) {
return "", false
return "", quoted, false
}
}
return raw, true
return raw, quoted, true
}

func isCookieNameValid(raw string) bool {
Expand Down
90 changes: 60 additions & 30 deletions src/net/http/cookie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ var writeSetCookiesTests = []struct {
&Cookie{Name: "a\rb", Value: "v"},
``,
},
// Quoted values (issue #46443)
{
&Cookie{Name: "cookie", Value: "quoted", Quoted: true},
`cookie="quoted"`,
},
{
&Cookie{Name: "cookie", Value: "quoted with spaces", Quoted: true},
`cookie="quoted with spaces"`,
},
{
&Cookie{Name: "cookie", Value: "quoted,with,commas", Quoted: true},
`cookie="quoted,with,commas"`,
},
}

func TestWriteSetCookies(t *testing.T) {
Expand Down Expand Up @@ -215,6 +228,15 @@ var addCookieTests = []struct {
},
"cookie-1=v$1; cookie-2=v$2; cookie-3=v$3",
},
// Quoted values (issue #46443)
{
[]*Cookie{
{Name: "cookie-1", Value: "quoted", Quoted: true},
{Name: "cookie-2", Value: "quoted with spaces", Quoted: true},
{Name: "cookie-3", Value: "quoted,with,commas", Quoted: true},
},
`cookie-1="quoted"; cookie-2="quoted with spaces"; cookie-3="quoted,with,commas"`,
},
}

func TestAddCookie(t *testing.T) {
Expand Down Expand Up @@ -326,37 +348,42 @@ var readSetCookiesTests = []struct {
},
{
Header{"Set-Cookie": {`special-2=" z"`}},
[]*Cookie{{Name: "special-2", Value: " z", Raw: `special-2=" z"`}},
[]*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
},
{
Header{"Set-Cookie": {`special-3="a "`}},
[]*Cookie{{Name: "special-3", Value: "a ", Raw: `special-3="a "`}},
[]*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
},
{
Header{"Set-Cookie": {`special-4=" "`}},
[]*Cookie{{Name: "special-4", Value: " ", Raw: `special-4=" "`}},
[]*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
},
{
Header{"Set-Cookie": {`special-5=a,z`}},
[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
},
{
Header{"Set-Cookie": {`special-6=",z"`}},
[]*Cookie{{Name: "special-6", Value: ",z", Raw: `special-6=",z"`}},
[]*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
},
{
Header{"Set-Cookie": {`special-7=a,`}},
[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
},
{
Header{"Set-Cookie": {`special-8=","`}},
[]*Cookie{{Name: "special-8", Value: ",", Raw: `special-8=","`}},
[]*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
},
// Make sure we can properly read back the Set-Cookie headers
// for names containing spaces:
{
Header{"Set-Cookie": {`special-9 =","`}},
[]*Cookie{{Name: "special-9", Value: ",", Raw: `special-9 =","`}},
[]*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
},
// Quoted values (issue #46443)
{
Header{"Set-Cookie": {`cookie="quoted"`}},
[]*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
},

// TODO(bradfitz): users have reported seeing this in the
Expand Down Expand Up @@ -425,15 +452,15 @@ var readCookiesTests = []struct {
Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
"",
[]*Cookie{
{Name: "Cookie-1", Value: "v$1"},
{Name: "c2", Value: "v2"},
{Name: "Cookie-1", Value: "v$1", Quoted: true},
{Name: "c2", Value: "v2", Quoted: true},
},
},
{
Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
"",
[]*Cookie{
{Name: "Cookie-1", Value: "v$1"},
{Name: "Cookie-1", Value: "v$1", Quoted: true},
{Name: "c2", Value: "v2"},
},
},
Expand Down Expand Up @@ -486,23 +513,26 @@ func TestCookieSanitizeValue(t *testing.T) {
log.SetOutput(&logbuf)

tests := []struct {
in, want string
in string
quoted bool
want string
}{
{"foo", "foo"},
{"foo;bar", "foobar"},
{"foo\\bar", "foobar"},
{"foo\"bar", "foobar"},
{"\x00\x7e\x7f\x80", "\x7e"},
{`"withquotes"`, "withquotes"},
{"a z", `"a z"`},
{" z", `" z"`},
{"a ", `"a "`},
{"a,z", `"a,z"`},
{",z", `",z"`},
{"a,", `"a,"`},
{"foo", false, "foo"},
{"foo;bar", false, "foobar"},
{"foo\\bar", false, "foobar"},
{"foo\"bar", false, "foobar"},
{"\x00\x7e\x7f\x80", false, "\x7e"},
{`withquotes`, true, `"withquotes"`},
{`"withquotes"`, true, `"withquotes"`}, // double quotes are not valid octets
{"a z", false, `"a z"`},
{" z", false, `" z"`},
{"a ", false, `"a "`},
{"a,z", false, `"a,z"`},
{",z", false, `",z"`},
{"a,", false, `"a,"`},
}
for _, tt := range tests {
if got := sanitizeCookieValue(tt.in); got != tt.want {
if got := sanitizeCookieValue(tt.in, tt.quoted); got != tt.want {
t.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
}
}
Expand Down Expand Up @@ -668,7 +698,7 @@ func TestParseCookie(t *testing.T) {
},
{
line: `Cookie-1="v$1";c2="v2"`,
cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}, {Name: "c2", Value: "v2"}},
cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1", Quoted: true}, {Name: "c2", Value: "v2", Quoted: true}},
},
{
line: "k1=",
Expand Down Expand Up @@ -800,37 +830,37 @@ func TestParseSetCookie(t *testing.T) {
},
{
line: `special-2=" z"`,
cookie: &Cookie{Name: "special-2", Value: " z", Raw: `special-2=" z"`},
cookie: &Cookie{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`},
},
{
line: `special-3="a "`,
cookie: &Cookie{Name: "special-3", Value: "a ", Raw: `special-3="a "`},
cookie: &Cookie{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`},
},
{
line: `special-4=" "`,
cookie: &Cookie{Name: "special-4", Value: " ", Raw: `special-4=" "`},
cookie: &Cookie{Name: "special-4", Value: " ", Quoted: true, 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"`},
cookie: &Cookie{Name: "special-6", Value: ",z", Quoted: true, 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=","`},
cookie: &Cookie{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`},
},
// Make sure we can properly read back the Set-Cookie headers
// for names containing spaces:
{
line: `special-9 =","`,
cookie: &Cookie{Name: "special-9", Value: ",", Raw: `special-9 =","`},
cookie: &Cookie{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`},
},
{
line: "",
Expand Down
4 changes: 3 additions & 1 deletion src/net/http/cookiejar/jar.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func New(o *Options) (*Jar, error) {
type entry struct {
Name string
Value string
Quoted bool
Domain string
Path string
SameSite string
Expand Down Expand Up @@ -220,7 +221,7 @@ func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
return s[i].seqNum < s[j].seqNum
})
for _, e := range selected {
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Quoted: e.Quoted})
}

return cookies
Expand Down Expand Up @@ -429,6 +430,7 @@ func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e e
}

e.Value = c.Value
e.Quoted = c.Quoted
e.Secure = c.Secure
e.HttpOnly = c.HttpOnly

Expand Down
Loading