Skip to content

Commit

Permalink
net/http: add ParseCookie, ParseSetCookie
Browse files Browse the repository at this point in the history
Fixes #66008

Change-Id: I64acb7da47a03bdef955f394682004906245a18b
Reviewed-on: https://go-review.googlesource.com/c/go/+/578275
Reviewed-by: Damien Neil <[email protected]>
Auto-Submit: Cherry Mui <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Cherry Mui <[email protected]>
  • Loading branch information
callthingsoff authored and pull[bot] committed Dec 28, 2024
1 parent 2499fb3 commit 6eb4764
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 88 deletions.
2 changes: 2 additions & 0 deletions api/next/66008.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pkg net/http, func ParseCookie(string) ([]*Cookie, error) #66008
pkg net/http, func ParseSetCookie(string) (*Cookie, error) #66008
7 changes: 7 additions & 0 deletions doc/next/6-stdlib/99-minor/net/http/66008.md
Original file line number Diff line number Diff line change
@@ -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.
218 changes: 130 additions & 88 deletions src/net/http/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 6eb4764

Please sign in to comment.