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 5package http 6 7import ( 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. 20type 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. 41func 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. 132func 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. 142func (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 198func 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. 237func 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. 248func 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. 256func 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 308var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 309 310func 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. 324func 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 335func 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 ";"> 341func sanitizeCookiePath(v string) string { 342 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 343} 344 345func validCookiePathByte(b byte) bool { 346 return 0x20 <= b && b < 0x7f && b != ';' 347} 348 349func 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 371func 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 384func isCookieNameValid(raw string) bool { 385 if raw == "" { 386 return false 387 } 388 return strings.IndexFunc(raw, isNotToken) < 0 389} 390