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 parsedPairs := 0 212 for i := 0; i < len(parts); i++ { 213 parts[i] = strings.TrimSpace(parts[i]) 214 if len(parts[i]) == 0 { 215 continue 216 } 217 name, val := parts[i], "" 218 if j := strings.Index(name, "="); j >= 0 { 219 name, val = name[:j], name[j+1:] 220 } 221 if !isCookieNameValid(name) { 222 continue 223 } 224 if filter != "" && filter != name { 225 continue 226 } 227 val, ok := parseCookieValue(val, true) 228 if !ok { 229 continue 230 } 231 cookies = append(cookies, &Cookie{Name: name, Value: val}) 232 parsedPairs++ 233 } 234 } 235 return cookies 236 } 237 238 // validCookieDomain returns whether v is a valid cookie domain-value. 239 func validCookieDomain(v string) bool { 240 if isCookieDomainName(v) { 241 return true 242 } 243 if net.ParseIP(v) != nil && !strings.Contains(v, ":") { 244 return true 245 } 246 return false 247 } 248 249 // validCookieExpires returns whether v is a valid cookie expires-value. 250 func validCookieExpires(t time.Time) bool { 251 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 252 return t.Year() >= 1601 253 } 254 255 // isCookieDomainName returns whether s is a valid domain name or a valid 256 // domain name with a leading dot '.'. It is almost a direct copy of 257 // package net's isDomainName. 258 func isCookieDomainName(s string) bool { 259 if len(s) == 0 { 260 return false 261 } 262 if len(s) > 255 { 263 return false 264 } 265 266 if s[0] == '.' { 267 // A cookie a domain attribute may start with a leading dot. 268 s = s[1:] 269 } 270 last := byte('.') 271 ok := false // Ok once we've seen a letter. 272 partlen := 0 273 for i := 0; i < len(s); i++ { 274 c := s[i] 275 switch { 276 default: 277 return false 278 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': 279 // No '_' allowed here (in contrast to package net). 280 ok = true 281 partlen++ 282 case '0' <= c && c <= '9': 283 // fine 284 partlen++ 285 case c == '-': 286 // Byte before dash cannot be dot. 287 if last == '.' { 288 return false 289 } 290 partlen++ 291 case c == '.': 292 // Byte before dot cannot be dot, dash. 293 if last == '.' || last == '-' { 294 return false 295 } 296 if partlen > 63 || partlen == 0 { 297 return false 298 } 299 partlen = 0 300 } 301 last = c 302 } 303 if last == '-' || partlen > 63 { 304 return false 305 } 306 307 return ok 308 } 309 310 var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 311 312 func sanitizeCookieName(n string) string { 313 return cookieNameSanitizer.Replace(n) 314 } 315 316 // http://tools.ietf.org/html/rfc6265#section-4.1.1 317 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) 318 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 319 // ; US-ASCII characters excluding CTLs, 320 // ; whitespace DQUOTE, comma, semicolon, 321 // ; and backslash 322 // We loosen this as spaces and commas are common in cookie values 323 // but we produce a quoted cookie-value in when value starts or ends 324 // with a comma or space. 325 // See https://golang.org/issue/7243 for the discussion. 326 func sanitizeCookieValue(v string) string { 327 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 328 if len(v) == 0 { 329 return v 330 } 331 if v[0] == ' ' || v[0] == ',' || v[len(v)-1] == ' ' || v[len(v)-1] == ',' { 332 return `"` + v + `"` 333 } 334 return v 335 } 336 337 func validCookieValueByte(b byte) bool { 338 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 339 } 340 341 // path-av = "Path=" path-value 342 // path-value = <any CHAR except CTLs or ";"> 343 func sanitizeCookiePath(v string) string { 344 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 345 } 346 347 func validCookiePathByte(b byte) bool { 348 return 0x20 <= b && b < 0x7f && b != ';' 349 } 350 351 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 352 ok := true 353 for i := 0; i < len(v); i++ { 354 if valid(v[i]) { 355 continue 356 } 357 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 358 ok = false 359 break 360 } 361 if ok { 362 return v 363 } 364 buf := make([]byte, 0, len(v)) 365 for i := 0; i < len(v); i++ { 366 if b := v[i]; valid(b) { 367 buf = append(buf, b) 368 } 369 } 370 return string(buf) 371 } 372 373 func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) { 374 // Strip the quotes, if present. 375 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { 376 raw = raw[1 : len(raw)-1] 377 } 378 for i := 0; i < len(raw); i++ { 379 if !validCookieValueByte(raw[i]) { 380 return "", false 381 } 382 } 383 return raw, true 384 } 385 386 func isCookieNameValid(raw string) bool { 387 if raw == "" { 388 return false 389 } 390 return strings.IndexFunc(raw, isNotToken) < 0 391 } 392