Skip to content

Instantly share code, notes, and snippets.

@Gurpartap
Created October 12, 2025 09:32
Show Gist options
  • Save Gurpartap/82fa91bbe45f439beb4e5b08894225d6 to your computer and use it in GitHub Desktop.
Save Gurpartap/82fa91bbe45f439beb4e5b08894225d6 to your computer and use it in GitHub Desktop.

Revisions

  1. Gurpartap created this gist Oct 12, 2025.
    183 changes: 183 additions & 0 deletions parse_set_cookie.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,183 @@
    package httputil

    import (
    "errors"
    "net/http"
    "net/textproto"
    "strconv"
    "strings"
    "time"
    "unicode/utf8"
    )

    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")
    )

    // ParseSetCookieRelaxed mirrors net/http.ParseSetCookie but allows quoted values
    // containing characters that ParseSetCookie would reject (notably '"').
    // The rest of the parsing logic matches the standard library implementation.
    func ParseSetCookieRelaxed(line string) (*http.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 !isTokenRelaxed(name) {
    return nil, errInvalidCookieName
    }
    value, quoted, ok := parseCookieValueRelaxed(value, true)
    if !ok {
    return nil, errInvalidCookieValue
    }

    cookie := &http.Cookie{
    Name: name,
    Value: value,
    Quoted: quoted,
    Raw: line,
    }

    for i := 1; i < len(parts); i++ {
    parts[i] = textproto.TrimString(parts[i])
    if parts[i] == "" {
    continue
    }

    attr, val, _ := strings.Cut(parts[i], "=")
    lowerAttr, asciiOnly := toLowerASCII(attr)
    if !asciiOnly {
    continue
    }

    parsedVal, _, ok := parseCookieValueRelaxed(val, false)
    if !ok {
    cookie.Unparsed = append(cookie.Unparsed, parts[i])
    continue
    }
    val = parsedVal

    handled := true
    switch lowerAttr {
    case "samesite":
    lowerVal, ascii := toLowerASCII(val)
    if !ascii {
    cookie.SameSite = http.SameSiteDefaultMode
    continue
    }
    switch lowerVal {
    case "lax":
    cookie.SameSite = http.SameSiteLaxMode
    case "strict":
    cookie.SameSite = http.SameSiteStrictMode
    case "none":
    cookie.SameSite = http.SameSiteNoneMode
    default:
    cookie.SameSite = http.SameSiteDefaultMode
    }
    continue
    case "secure":
    cookie.Secure = true
    continue
    case "httponly":
    cookie.HttpOnly = true
    continue
    case "domain":
    cookie.Domain = val
    continue
    case "max-age":
    secs, parseErr := strconv.Atoi(val)
    if parseErr != nil || (secs != 0 && len(val) > 0 && val[0] == '0') {
    handled = false
    break
    }
    if secs <= 0 {
    secs = -1
    }
    cookie.MaxAge = secs
    continue
    case "expires":
    cookie.RawExpires = val
    exptime, parseErr := time.Parse(time.RFC1123, val)
    if parseErr != nil {
    exptime, parseErr = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
    if parseErr != nil {
    cookie.Expires = time.Time{}
    handled = false
    break
    }
    }
    cookie.Expires = exptime.UTC()
    continue
    case "path":
    cookie.Path = val
    continue
    case "partitioned":
    cookie.Partitioned = true
    continue
    default:
    handled = false
    }

    if !handled {
    cookie.Unparsed = append(cookie.Unparsed, parts[i])
    }
    }

    return cookie, nil
    }

    func parseCookieValueRelaxed(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) {
    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 !validCookieValueByteRelaxed(raw[i]) {
    return "", quoted, false
    }
    }
    return raw, quoted, true
    }

    func validCookieValueByteRelaxed(b byte) bool {
    return 0x20 <= b && b < 0x7f && b != ';' && b != '\\'
    }

    func toLowerASCII(s string) (string, bool) {
    for i := 0; i < len(s); i++ {
    if s[i] >= utf8.RuneSelf {
    return s, false
    }
    }
    return strings.ToLower(s), true
    }

    func isTokenRelaxed(v string) bool {
    if v == "" {
    return false
    }
    for i := 0; i < len(v); i++ {
    b := v[i]
    if b >= utf8.RuneSelf {
    return false
    }
    if 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z' || '0' <= b && b <= '9' {
    continue
    }
    switch b {
    case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~':
    continue
    }
    return false
    }
    return true
    }