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