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