github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/net/http/cookie.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http 6 7 import ( 8 "errors" 9 "fmt" 10 "log" 11 "net" 12 "net/http/internal/ascii" 13 "net/textproto" 14 "strconv" 15 "strings" 16 "time" 17 ) 18 19 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an 20 // HTTP response or the Cookie header of an HTTP request. 21 // 22 // See https://tools.ietf.org/html/rfc6265 for details. 23 type Cookie struct { 24 Name string 25 Value string 26 27 Path string // optional 28 Domain string // optional 29 Expires time.Time // optional 30 RawExpires string // for reading cookies only 31 32 // MaxAge=0 means no 'Max-Age' attribute specified. 33 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 34 // MaxAge>0 means Max-Age attribute present and given in seconds 35 MaxAge int 36 Secure bool 37 HttpOnly bool 38 SameSite SameSite 39 Raw string 40 Unparsed []string // Raw text of unparsed attribute-value pairs 41 } 42 43 // SameSite allows a server to define a cookie attribute making it impossible for 44 // the browser to send this cookie along with cross-site requests. The main 45 // goal is to mitigate the risk of cross-origin information leakage, and provide 46 // some protection against cross-site request forgery attacks. 47 // 48 // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. 49 type SameSite int 50 51 const ( 52 SameSiteDefaultMode SameSite = iota + 1 53 SameSiteLaxMode 54 SameSiteStrictMode 55 SameSiteNoneMode 56 ) 57 58 // readSetCookies parses all "Set-Cookie" values from 59 // the header h and returns the successfully parsed Cookies. 60 func readSetCookies(h Header) []*Cookie { 61 cookieCount := len(h["Set-Cookie"]) 62 if cookieCount == 0 { 63 return []*Cookie{} 64 } 65 cookies := make([]*Cookie, 0, cookieCount) 66 for _, line := range h["Set-Cookie"] { 67 parts := strings.Split(textproto.TrimString(line), ";") 68 if len(parts) == 1 && parts[0] == "" { 69 continue 70 } 71 parts[0] = textproto.TrimString(parts[0]) 72 name, value, ok := strings.Cut(parts[0], "=") 73 if !ok { 74 continue 75 } 76 name = textproto.TrimString(name) 77 if !isCookieNameValid(name) { 78 continue 79 } 80 value, ok = parseCookieValue(value, true) 81 if !ok { 82 continue 83 } 84 c := &Cookie{ 85 Name: name, 86 Value: value, 87 Raw: line, 88 } 89 for i := 1; i < len(parts); i++ { 90 parts[i] = textproto.TrimString(parts[i]) 91 if len(parts[i]) == 0 { 92 continue 93 } 94 95 attr, val, _ := strings.Cut(parts[i], "=") 96 lowerAttr, isASCII := ascii.ToLower(attr) 97 if !isASCII { 98 continue 99 } 100 val, ok = parseCookieValue(val, false) 101 if !ok { 102 c.Unparsed = append(c.Unparsed, parts[i]) 103 continue 104 } 105 106 switch lowerAttr { 107 case "samesite": 108 lowerVal, ascii := ascii.ToLower(val) 109 if !ascii { 110 c.SameSite = SameSiteDefaultMode 111 continue 112 } 113 switch lowerVal { 114 case "lax": 115 c.SameSite = SameSiteLaxMode 116 case "strict": 117 c.SameSite = SameSiteStrictMode 118 case "none": 119 c.SameSite = SameSiteNoneMode 120 default: 121 c.SameSite = SameSiteDefaultMode 122 } 123 continue 124 case "secure": 125 c.Secure = true 126 continue 127 case "httponly": 128 c.HttpOnly = true 129 continue 130 case "domain": 131 c.Domain = val 132 continue 133 case "max-age": 134 secs, err := strconv.Atoi(val) 135 if err != nil || secs != 0 && val[0] == '0' { 136 break 137 } 138 if secs <= 0 { 139 secs = -1 140 } 141 c.MaxAge = secs 142 continue 143 case "expires": 144 c.RawExpires = val 145 exptime, err := time.Parse(time.RFC1123, val) 146 if err != nil { 147 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) 148 if err != nil { 149 c.Expires = time.Time{} 150 break 151 } 152 } 153 c.Expires = exptime.UTC() 154 continue 155 case "path": 156 c.Path = val 157 continue 158 } 159 c.Unparsed = append(c.Unparsed, parts[i]) 160 } 161 cookies = append(cookies, c) 162 } 163 return cookies 164 } 165 166 // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. 167 // The provided cookie must have a valid Name. Invalid cookies may be 168 // silently dropped. 169 func SetCookie(w ResponseWriter, cookie *Cookie) { 170 if v := cookie.String(); v != "" { 171 w.Header().Add("Set-Cookie", v) 172 } 173 } 174 175 // String returns the serialization of the cookie for use in a Cookie 176 // header (if only Name and Value are set) or a Set-Cookie response 177 // header (if other fields are set). 178 // If c is nil or c.Name is invalid, the empty string is returned. 179 func (c *Cookie) String() string { 180 if c == nil || !isCookieNameValid(c.Name) { 181 return "" 182 } 183 // extraCookieLength derived from typical length of cookie attributes 184 // see RFC 6265 Sec 4.1. 185 const extraCookieLength = 110 186 var b strings.Builder 187 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength) 188 b.WriteString(c.Name) 189 b.WriteRune('=') 190 b.WriteString(sanitizeCookieValue(c.Value)) 191 192 if len(c.Path) > 0 { 193 b.WriteString("; Path=") 194 b.WriteString(sanitizeCookiePath(c.Path)) 195 } 196 if len(c.Domain) > 0 { 197 if validCookieDomain(c.Domain) { 198 // A c.Domain containing illegal characters is not 199 // sanitized but simply dropped which turns the cookie 200 // into a host-only cookie. A leading dot is okay 201 // but won't be sent. 202 d := c.Domain 203 if d[0] == '.' { 204 d = d[1:] 205 } 206 b.WriteString("; Domain=") 207 b.WriteString(d) 208 } else { 209 log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain) 210 } 211 } 212 var buf [len(TimeFormat)]byte 213 if validCookieExpires(c.Expires) { 214 b.WriteString("; Expires=") 215 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat)) 216 } 217 if c.MaxAge > 0 { 218 b.WriteString("; Max-Age=") 219 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10)) 220 } else if c.MaxAge < 0 { 221 b.WriteString("; Max-Age=0") 222 } 223 if c.HttpOnly { 224 b.WriteString("; HttpOnly") 225 } 226 if c.Secure { 227 b.WriteString("; Secure") 228 } 229 switch c.SameSite { 230 case SameSiteDefaultMode: 231 // Skip, default mode is obtained by not emitting the attribute. 232 case SameSiteNoneMode: 233 b.WriteString("; SameSite=None") 234 case SameSiteLaxMode: 235 b.WriteString("; SameSite=Lax") 236 case SameSiteStrictMode: 237 b.WriteString("; SameSite=Strict") 238 } 239 return b.String() 240 } 241 242 // Valid reports whether the cookie is valid. 243 func (c *Cookie) Valid() error { 244 if c == nil { 245 return errors.New("http: nil Cookie") 246 } 247 if !isCookieNameValid(c.Name) { 248 return errors.New("http: invalid Cookie.Name") 249 } 250 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) { 251 return errors.New("http: invalid Cookie.Expires") 252 } 253 for i := 0; i < len(c.Value); i++ { 254 if !validCookieValueByte(c.Value[i]) { 255 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i]) 256 } 257 } 258 if len(c.Path) > 0 { 259 for i := 0; i < len(c.Path); i++ { 260 if !validCookiePathByte(c.Path[i]) { 261 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i]) 262 } 263 } 264 } 265 if len(c.Domain) > 0 { 266 if !validCookieDomain(c.Domain) { 267 return errors.New("http: invalid Cookie.Domain") 268 } 269 } 270 return nil 271 } 272 273 // readCookies parses all "Cookie" values from the header h and 274 // returns the successfully parsed Cookies. 275 // 276 // if filter isn't empty, only cookies of that name are returned. 277 func readCookies(h Header, filter string) []*Cookie { 278 lines := h["Cookie"] 279 if len(lines) == 0 { 280 return []*Cookie{} 281 } 282 283 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) 284 for _, line := range lines { 285 line = textproto.TrimString(line) 286 287 var part string 288 for len(line) > 0 { // continue since we have rest 289 part, line, _ = strings.Cut(line, ";") 290 part = textproto.TrimString(part) 291 if part == "" { 292 continue 293 } 294 name, val, _ := strings.Cut(part, "=") 295 name = textproto.TrimString(name) 296 if !isCookieNameValid(name) { 297 continue 298 } 299 if filter != "" && filter != name { 300 continue 301 } 302 val, ok := parseCookieValue(val, true) 303 if !ok { 304 continue 305 } 306 cookies = append(cookies, &Cookie{Name: name, Value: val}) 307 } 308 } 309 return cookies 310 } 311 312 // validCookieDomain reports whether v is a valid cookie domain-value. 313 func validCookieDomain(v string) bool { 314 if isCookieDomainName(v) { 315 return true 316 } 317 if net.ParseIP(v) != nil && !strings.Contains(v, ":") { 318 return true 319 } 320 return false 321 } 322 323 // validCookieExpires reports whether v is a valid cookie expires-value. 324 func validCookieExpires(t time.Time) bool { 325 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 326 return t.Year() >= 1601 327 } 328 329 // isCookieDomainName reports whether s is a valid domain name or a valid 330 // domain name with a leading dot '.'. It is almost a direct copy of 331 // package net's isDomainName. 332 func isCookieDomainName(s string) bool { 333 if len(s) == 0 { 334 return false 335 } 336 if len(s) > 255 { 337 return false 338 } 339 340 if s[0] == '.' { 341 // A cookie a domain attribute may start with a leading dot. 342 s = s[1:] 343 } 344 last := byte('.') 345 ok := false // Ok once we've seen a letter. 346 partlen := 0 347 for i := 0; i < len(s); i++ { 348 c := s[i] 349 switch { 350 default: 351 return false 352 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': 353 // No '_' allowed here (in contrast to package net). 354 ok = true 355 partlen++ 356 case '0' <= c && c <= '9': 357 // fine 358 partlen++ 359 case c == '-': 360 // Byte before dash cannot be dot. 361 if last == '.' { 362 return false 363 } 364 partlen++ 365 case c == '.': 366 // Byte before dot cannot be dot, dash. 367 if last == '.' || last == '-' { 368 return false 369 } 370 if partlen > 63 || partlen == 0 { 371 return false 372 } 373 partlen = 0 374 } 375 last = c 376 } 377 if last == '-' || partlen > 63 { 378 return false 379 } 380 381 return ok 382 } 383 384 var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 385 386 func sanitizeCookieName(n string) string { 387 return cookieNameSanitizer.Replace(n) 388 } 389 390 // sanitizeCookieValue produces a suitable cookie-value from v. 391 // https://tools.ietf.org/html/rfc6265#section-4.1.1 392 // 393 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) 394 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 395 // ; US-ASCII characters excluding CTLs, 396 // ; whitespace DQUOTE, comma, semicolon, 397 // ; and backslash 398 // 399 // We loosen this as spaces and commas are common in cookie values 400 // but we produce a quoted cookie-value if and only if v contains 401 // commas or spaces. 402 // See https://golang.org/issue/7243 for the discussion. 403 func sanitizeCookieValue(v string) string { 404 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 405 if len(v) == 0 { 406 return v 407 } 408 if strings.ContainsAny(v, " ,") { 409 return `"` + v + `"` 410 } 411 return v 412 } 413 414 func validCookieValueByte(b byte) bool { 415 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 416 } 417 418 // path-av = "Path=" path-value 419 // path-value = <any CHAR except CTLs or ";"> 420 func sanitizeCookiePath(v string) string { 421 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 422 } 423 424 func validCookiePathByte(b byte) bool { 425 return 0x20 <= b && b < 0x7f && b != ';' 426 } 427 428 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 429 ok := true 430 for i := 0; i < len(v); i++ { 431 if valid(v[i]) { 432 continue 433 } 434 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 435 ok = false 436 break 437 } 438 if ok { 439 return v 440 } 441 buf := make([]byte, 0, len(v)) 442 for i := 0; i < len(v); i++ { 443 if b := v[i]; valid(b) { 444 buf = append(buf, b) 445 } 446 } 447 return string(buf) 448 } 449 450 func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) { 451 // Strip the quotes, if present. 452 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { 453 raw = raw[1 : len(raw)-1] 454 } 455 for i := 0; i < len(raw); i++ { 456 if !validCookieValueByte(raw[i]) { 457 return "", false 458 } 459 } 460 return raw, true 461 } 462 463 func isCookieNameValid(raw string) bool { 464 if raw == "" { 465 return false 466 } 467 return strings.IndexFunc(raw, isNotToken) < 0 468 }