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