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