package http
import (
"errors"
"fmt"
"log"
"net"
"net/http/internal/ascii"
"net/textproto"
"strconv"
"strings"
"time"
)
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time .Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}
type SameSite int
const (
SameSiteDefaultMode SameSite = iota + 1
SameSiteLaxMode
SameSiteStrictMode
SameSiteNoneMode
)
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
}
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
}
func SetCookie (w ResponseWriter , cookie *Cookie ) {
if v := cookie .String (); v != "" {
w .Header ().Add ("Set-Cookie" , v )
}
}
func (c *Cookie ) String () string {
if c == nil || !isCookieNameValid (c .Name ) {
return ""
}
const extraCookieLength = 110
var b strings .Builder
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 ))
if len (c .Path ) > 0 {
b .WriteString ("; Path=" )
b .WriteString (sanitizeCookiePath (c .Path ))
}
if len (c .Domain ) > 0 {
if validCookieDomain (c .Domain ) {
d := c .Domain
if d [0 ] == '.' {
d = d [1 :]
}
b .WriteString ("; Domain=" )
b .WriteString (d )
} else {
log .Printf ("net/http: invalid Cookie.Domain %q; dropping domain attribute" , c .Domain )
}
}
var buf [len (TimeFormat )]byte
if validCookieExpires (c .Expires ) {
b .WriteString ("; Expires=" )
b .Write (c .Expires .UTC ().AppendFormat (buf [:0 ], TimeFormat ))
}
if c .MaxAge > 0 {
b .WriteString ("; Max-Age=" )
b .Write (strconv .AppendInt (buf [:0 ], int64 (c .MaxAge ), 10 ))
} else if c .MaxAge < 0 {
b .WriteString ("; Max-Age=0" )
}
if c .HttpOnly {
b .WriteString ("; HttpOnly" )
}
if c .Secure {
b .WriteString ("; Secure" )
}
switch c .SameSite {
case SameSiteDefaultMode :
case SameSiteNoneMode :
b .WriteString ("; SameSite=None" )
case SameSiteLaxMode :
b .WriteString ("; SameSite=Lax" )
case SameSiteStrictMode :
b .WriteString ("; SameSite=Strict" )
}
return b .String ()
}
func (c *Cookie ) Valid () error {
if c == nil {
return errors .New ("http: nil Cookie" )
}
if !isCookieNameValid (c .Name ) {
return errors .New ("http: invalid Cookie.Name" )
}
if !c .Expires .IsZero () && !validCookieExpires (c .Expires ) {
return errors .New ("http: invalid Cookie.Expires" )
}
for i := 0 ; i < len (c .Value ); i ++ {
if !validCookieValueByte (c .Value [i ]) {
return fmt .Errorf ("http: invalid byte %q in Cookie.Value" , c .Value [i ])
}
}
if len (c .Path ) > 0 {
for i := 0 ; i < len (c .Path ); i ++ {
if !validCookiePathByte (c .Path [i ]) {
return fmt .Errorf ("http: invalid byte %q in Cookie.Path" , c .Path [i ])
}
}
}
if len (c .Domain ) > 0 {
if !validCookieDomain (c .Domain ) {
return errors .New ("http: invalid Cookie.Domain" )
}
}
return nil
}
func readCookies (h Header , filter string ) []*Cookie {
lines := h ["Cookie" ]
if len (lines ) == 0 {
return []*Cookie {}
}
cookies := make ([]*Cookie , 0 , len (lines )+strings .Count (lines [0 ], ";" ))
for _ , line := range lines {
line = textproto .TrimString (line )
var part string
for len (line ) > 0 {
part , line , _ = strings .Cut (line , ";" )
part = textproto .TrimString (part )
if part == "" {
continue
}
name , val , _ := strings .Cut (part , "=" )
name = textproto .TrimString (name )
if !isCookieNameValid (name ) {
continue
}
if filter != "" && filter != name {
continue
}
val , ok := parseCookieValue (val , true )
if !ok {
continue
}
cookies = append (cookies , &Cookie {Name : name , Value : val })
}
}
return cookies
}
func validCookieDomain (v string ) bool {
if isCookieDomainName (v ) {
return true
}
if net .ParseIP (v ) != nil && !strings .Contains (v , ":" ) {
return true
}
return false
}
func validCookieExpires (t time .Time ) bool {
return t .Year () >= 1601
}
func isCookieDomainName (s string ) bool {
if len (s ) == 0 {
return false
}
if len (s ) > 255 {
return false
}
if s [0 ] == '.' {
s = s [1 :]
}
last := byte ('.' )
ok := false
partlen := 0
for i := 0 ; i < len (s ); i ++ {
c := s [i ]
switch {
default :
return false
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' :
ok = true
partlen ++
case '0' <= c && c <= '9' :
partlen ++
case c == '-' :
if last == '.' {
return false
}
partlen ++
case c == '.' :
if last == '.' || last == '-' {
return false
}
if partlen > 63 || partlen == 0 {
return false
}
partlen = 0
}
last = c
}
if last == '-' || partlen > 63 {
return false
}
return ok
}
var cookieNameSanitizer = strings .NewReplacer ("\n" , "-" , "\r" , "-" )
func sanitizeCookieName (n string ) string {
return cookieNameSanitizer .Replace (n )
}
func sanitizeCookieValue (v string ) string {
v = sanitizeOrWarn ("Cookie.Value" , validCookieValueByte , v )
if len (v ) == 0 {
return v
}
if strings .ContainsAny (v , " ," ) {
return `"` + v + `"`
}
return v
}
func validCookieValueByte (b byte ) bool {
return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
}
func sanitizeCookiePath (v string ) string {
return sanitizeOrWarn ("Cookie.Path" , validCookiePathByte , v )
}
func validCookiePathByte (b byte ) bool {
return 0x20 <= b && b < 0x7f && b != ';'
}
func sanitizeOrWarn (fieldName string , valid func (byte ) bool , v string ) string {
ok := true
for i := 0 ; i < len (v ); i ++ {
if valid (v [i ]) {
continue
}
log .Printf ("net/http: invalid byte %q in %s; dropping invalid bytes" , v [i ], fieldName )
ok = false
break
}
if ok {
return v
}
buf := make ([]byte , 0 , len (v ))
for i := 0 ; i < len (v ); i ++ {
if b := v [i ]; valid (b ) {
buf = append (buf , b )
}
}
return string (buf )
}
func parseCookieValue (raw string , allowDoubleQuote bool ) (string , bool ) {
if allowDoubleQuote && len (raw ) > 1 && raw [0 ] == '"' && raw [len (raw )-1 ] == '"' {
raw = raw [1 : len (raw )-1 ]
}
for i := 0 ; i < len (raw ); i ++ {
if !validCookieValueByte (raw [i ]) {
return "" , false
}
}
return raw , true
}
func isCookieNameValid (raw string ) bool {
if raw == "" {
return false
}
return strings .IndexFunc (raw , isNotToken ) < 0
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .