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