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