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 "errors" 9 "fmt" 10 "log" 11 "net" 12 "net/http/internal/ascii" 13 "net/textproto" 14 "strconv" 15 "strings" 16 "time" 17) 18 19// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an 20// HTTP response or the Cookie header of an HTTP request. 21// 22// See https://tools.ietf.org/html/rfc6265 for details. 23type Cookie struct { 24 Name string 25 Value string 26 Quoted bool // indicates whether the Value was originally quoted 27 28 Path string // optional 29 Domain string // optional 30 Expires time.Time // optional 31 RawExpires string // for reading cookies only 32 33 // MaxAge=0 means no 'Max-Age' attribute specified. 34 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 35 // MaxAge>0 means Max-Age attribute present and given in seconds 36 MaxAge int 37 Secure bool 38 HttpOnly bool 39 SameSite SameSite 40 Partitioned bool 41 Raw string 42 Unparsed []string // Raw text of unparsed attribute-value pairs 43} 44 45// SameSite allows a server to define a cookie attribute making it impossible for 46// the browser to send this cookie along with cross-site requests. The main 47// goal is to mitigate the risk of cross-origin information leakage, and provide 48// some protection against cross-site request forgery attacks. 49// 50// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. 51type SameSite int 52 53const ( 54 SameSiteDefaultMode SameSite = iota + 1 55 SameSiteLaxMode 56 SameSiteStrictMode 57 SameSiteNoneMode 58) 59 60var ( 61 errBlankCookie = errors.New("http: blank cookie") 62 errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie") 63 errInvalidCookieName = errors.New("http: invalid cookie name") 64 errInvalidCookieValue = errors.New("http: invalid cookie value") 65) 66 67// ParseCookie parses a Cookie header value and returns all the cookies 68// which were set in it. Since the same cookie name can appear multiple times 69// the returned Values can contain more than one value for a given key. 70func ParseCookie(line string) ([]*Cookie, error) { 71 parts := strings.Split(textproto.TrimString(line), ";") 72 if len(parts) == 1 && parts[0] == "" { 73 return nil, errBlankCookie 74 } 75 cookies := make([]*Cookie, 0, len(parts)) 76 for _, s := range parts { 77 s = textproto.TrimString(s) 78 name, value, found := strings.Cut(s, "=") 79 if !found { 80 return nil, errEqualNotFoundInCookie 81 } 82 if !isCookieNameValid(name) { 83 return nil, errInvalidCookieName 84 } 85 value, quoted, found := parseCookieValue(value, true) 86 if !found { 87 return nil, errInvalidCookieValue 88 } 89 cookies = append(cookies, &Cookie{Name: name, Value: value, Quoted: quoted}) 90 } 91 return cookies, nil 92} 93 94// ParseSetCookie parses a Set-Cookie header value and returns a cookie. 95// It returns an error on syntax error. 96func ParseSetCookie(line string) (*Cookie, error) { 97 parts := strings.Split(textproto.TrimString(line), ";") 98 if len(parts) == 1 && parts[0] == "" { 99 return nil, errBlankCookie 100 } 101 parts[0] = textproto.TrimString(parts[0]) 102 name, value, ok := strings.Cut(parts[0], "=") 103 if !ok { 104 return nil, errEqualNotFoundInCookie 105 } 106 name = textproto.TrimString(name) 107 if !isCookieNameValid(name) { 108 return nil, errInvalidCookieName 109 } 110 value, quoted, ok := parseCookieValue(value, true) 111 if !ok { 112 return nil, errInvalidCookieValue 113 } 114 c := &Cookie{ 115 Name: name, 116 Value: value, 117 Quoted: quoted, 118 Raw: line, 119 } 120 for i := 1; i < len(parts); i++ { 121 parts[i] = textproto.TrimString(parts[i]) 122 if len(parts[i]) == 0 { 123 continue 124 } 125 126 attr, val, _ := strings.Cut(parts[i], "=") 127 lowerAttr, isASCII := ascii.ToLower(attr) 128 if !isASCII { 129 continue 130 } 131 val, _, ok = parseCookieValue(val, false) 132 if !ok { 133 c.Unparsed = append(c.Unparsed, parts[i]) 134 continue 135 } 136 137 switch lowerAttr { 138 case "samesite": 139 lowerVal, ascii := ascii.ToLower(val) 140 if !ascii { 141 c.SameSite = SameSiteDefaultMode 142 continue 143 } 144 switch lowerVal { 145 case "lax": 146 c.SameSite = SameSiteLaxMode 147 case "strict": 148 c.SameSite = SameSiteStrictMode 149 case "none": 150 c.SameSite = SameSiteNoneMode 151 default: 152 c.SameSite = SameSiteDefaultMode 153 } 154 continue 155 case "secure": 156 c.Secure = true 157 continue 158 case "httponly": 159 c.HttpOnly = true 160 continue 161 case "domain": 162 c.Domain = val 163 continue 164 case "max-age": 165 secs, err := strconv.Atoi(val) 166 if err != nil || secs != 0 && val[0] == '0' { 167 break 168 } 169 if secs <= 0 { 170 secs = -1 171 } 172 c.MaxAge = secs 173 continue 174 case "expires": 175 c.RawExpires = val 176 exptime, err := time.Parse(time.RFC1123, val) 177 if err != nil { 178 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) 179 if err != nil { 180 c.Expires = time.Time{} 181 break 182 } 183 } 184 c.Expires = exptime.UTC() 185 continue 186 case "path": 187 c.Path = val 188 continue 189 case "partitioned": 190 c.Partitioned = true 191 continue 192 } 193 c.Unparsed = append(c.Unparsed, parts[i]) 194 } 195 return c, nil 196} 197 198// readSetCookies parses all "Set-Cookie" values from 199// the header h and returns the successfully parsed Cookies. 200func readSetCookies(h Header) []*Cookie { 201 cookieCount := len(h["Set-Cookie"]) 202 if cookieCount == 0 { 203 return []*Cookie{} 204 } 205 cookies := make([]*Cookie, 0, cookieCount) 206 for _, line := range h["Set-Cookie"] { 207 if cookie, err := ParseSetCookie(line); err == nil { 208 cookies = append(cookies, cookie) 209 } 210 } 211 return cookies 212} 213 214// SetCookie adds a Set-Cookie header to the provided [ResponseWriter]'s headers. 215// The provided cookie must have a valid Name. Invalid cookies may be 216// silently dropped. 217func SetCookie(w ResponseWriter, cookie *Cookie) { 218 if v := cookie.String(); v != "" { 219 w.Header().Add("Set-Cookie", v) 220 } 221} 222 223// String returns the serialization of the cookie for use in a [Cookie] 224// header (if only Name and Value are set) or a Set-Cookie response 225// header (if other fields are set). 226// If c is nil or c.Name is invalid, the empty string is returned. 227func (c *Cookie) String() string { 228 if c == nil || !isCookieNameValid(c.Name) { 229 return "" 230 } 231 // extraCookieLength derived from typical length of cookie attributes 232 // see RFC 6265 Sec 4.1. 233 const extraCookieLength = 110 234 var b strings.Builder 235 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength) 236 b.WriteString(c.Name) 237 b.WriteRune('=') 238 b.WriteString(sanitizeCookieValue(c.Value, c.Quoted)) 239 240 if len(c.Path) > 0 { 241 b.WriteString("; Path=") 242 b.WriteString(sanitizeCookiePath(c.Path)) 243 } 244 if len(c.Domain) > 0 { 245 if validCookieDomain(c.Domain) { 246 // A c.Domain containing illegal characters is not 247 // sanitized but simply dropped which turns the cookie 248 // into a host-only cookie. A leading dot is okay 249 // but won't be sent. 250 d := c.Domain 251 if d[0] == '.' { 252 d = d[1:] 253 } 254 b.WriteString("; Domain=") 255 b.WriteString(d) 256 } else { 257 log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain) 258 } 259 } 260 var buf [len(TimeFormat)]byte 261 if validCookieExpires(c.Expires) { 262 b.WriteString("; Expires=") 263 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat)) 264 } 265 if c.MaxAge > 0 { 266 b.WriteString("; Max-Age=") 267 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10)) 268 } else if c.MaxAge < 0 { 269 b.WriteString("; Max-Age=0") 270 } 271 if c.HttpOnly { 272 b.WriteString("; HttpOnly") 273 } 274 if c.Secure { 275 b.WriteString("; Secure") 276 } 277 switch c.SameSite { 278 case SameSiteDefaultMode: 279 // Skip, default mode is obtained by not emitting the attribute. 280 case SameSiteNoneMode: 281 b.WriteString("; SameSite=None") 282 case SameSiteLaxMode: 283 b.WriteString("; SameSite=Lax") 284 case SameSiteStrictMode: 285 b.WriteString("; SameSite=Strict") 286 } 287 if c.Partitioned { 288 b.WriteString("; Partitioned") 289 } 290 return b.String() 291} 292 293// Valid reports whether the cookie is valid. 294func (c *Cookie) Valid() error { 295 if c == nil { 296 return errors.New("http: nil Cookie") 297 } 298 if !isCookieNameValid(c.Name) { 299 return errors.New("http: invalid Cookie.Name") 300 } 301 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) { 302 return errors.New("http: invalid Cookie.Expires") 303 } 304 for i := 0; i < len(c.Value); i++ { 305 if !validCookieValueByte(c.Value[i]) { 306 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i]) 307 } 308 } 309 if len(c.Path) > 0 { 310 for i := 0; i < len(c.Path); i++ { 311 if !validCookiePathByte(c.Path[i]) { 312 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i]) 313 } 314 } 315 } 316 if len(c.Domain) > 0 { 317 if !validCookieDomain(c.Domain) { 318 return errors.New("http: invalid Cookie.Domain") 319 } 320 } 321 if c.Partitioned { 322 if !c.Secure { 323 return errors.New("http: partitioned cookies must be set with Secure") 324 } 325 } 326 return nil 327} 328 329// readCookies parses all "Cookie" values from the header h and 330// returns the successfully parsed Cookies. 331// 332// if filter isn't empty, only cookies of that name are returned. 333func readCookies(h Header, filter string) []*Cookie { 334 lines := h["Cookie"] 335 if len(lines) == 0 { 336 return []*Cookie{} 337 } 338 339 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) 340 for _, line := range lines { 341 line = textproto.TrimString(line) 342 343 var part string 344 for len(line) > 0 { // continue since we have rest 345 part, line, _ = strings.Cut(line, ";") 346 part = textproto.TrimString(part) 347 if part == "" { 348 continue 349 } 350 name, val, _ := strings.Cut(part, "=") 351 name = textproto.TrimString(name) 352 if !isCookieNameValid(name) { 353 continue 354 } 355 if filter != "" && filter != name { 356 continue 357 } 358 val, quoted, ok := parseCookieValue(val, true) 359 if !ok { 360 continue 361 } 362 cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted}) 363 } 364 } 365 return cookies 366} 367 368// validCookieDomain reports whether v is a valid cookie domain-value. 369func validCookieDomain(v string) bool { 370 if isCookieDomainName(v) { 371 return true 372 } 373 if net.ParseIP(v) != nil && !strings.Contains(v, ":") { 374 return true 375 } 376 return false 377} 378 379// validCookieExpires reports whether v is a valid cookie expires-value. 380func validCookieExpires(t time.Time) bool { 381 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 382 return t.Year() >= 1601 383} 384 385// isCookieDomainName reports whether s is a valid domain name or a valid 386// domain name with a leading dot '.'. It is almost a direct copy of 387// package net's isDomainName. 388func isCookieDomainName(s string) bool { 389 if len(s) == 0 { 390 return false 391 } 392 if len(s) > 255 { 393 return false 394 } 395 396 if s[0] == '.' { 397 // A cookie a domain attribute may start with a leading dot. 398 s = s[1:] 399 } 400 last := byte('.') 401 ok := false // Ok once we've seen a letter. 402 partlen := 0 403 for i := 0; i < len(s); i++ { 404 c := s[i] 405 switch { 406 default: 407 return false 408 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': 409 // No '_' allowed here (in contrast to package net). 410 ok = true 411 partlen++ 412 case '0' <= c && c <= '9': 413 // fine 414 partlen++ 415 case c == '-': 416 // Byte before dash cannot be dot. 417 if last == '.' { 418 return false 419 } 420 partlen++ 421 case c == '.': 422 // Byte before dot cannot be dot, dash. 423 if last == '.' || last == '-' { 424 return false 425 } 426 if partlen > 63 || partlen == 0 { 427 return false 428 } 429 partlen = 0 430 } 431 last = c 432 } 433 if last == '-' || partlen > 63 { 434 return false 435 } 436 437 return ok 438} 439 440var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 441 442func sanitizeCookieName(n string) string { 443 return cookieNameSanitizer.Replace(n) 444} 445 446// sanitizeCookieValue produces a suitable cookie-value from v. 447// It receives a quoted bool indicating whether the value was originally 448// quoted. 449// https://tools.ietf.org/html/rfc6265#section-4.1.1 450// 451// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) 452// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 453// ; US-ASCII characters excluding CTLs, 454// ; whitespace DQUOTE, comma, semicolon, 455// ; and backslash 456// 457// We loosen this as spaces and commas are common in cookie values 458// thus we produce a quoted cookie-value if v contains commas or spaces. 459// See https://golang.org/issue/7243 for the discussion. 460func sanitizeCookieValue(v string, quoted bool) string { 461 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 462 if len(v) == 0 { 463 return v 464 } 465 if strings.ContainsAny(v, " ,") || quoted { 466 return `"` + v + `"` 467 } 468 return v 469} 470 471func validCookieValueByte(b byte) bool { 472 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 473} 474 475// path-av = "Path=" path-value 476// path-value = <any CHAR except CTLs or ";"> 477func sanitizeCookiePath(v string) string { 478 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 479} 480 481func validCookiePathByte(b byte) bool { 482 return 0x20 <= b && b < 0x7f && b != ';' 483} 484 485func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 486 ok := true 487 for i := 0; i < len(v); i++ { 488 if valid(v[i]) { 489 continue 490 } 491 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 492 ok = false 493 break 494 } 495 if ok { 496 return v 497 } 498 buf := make([]byte, 0, len(v)) 499 for i := 0; i < len(v); i++ { 500 if b := v[i]; valid(b) { 501 buf = append(buf, b) 502 } 503 } 504 return string(buf) 505} 506 507// parseCookieValue parses a cookie value according to RFC 6265. 508// If allowDoubleQuote is true, parseCookieValue will consider that it 509// is parsing the cookie-value; 510// otherwise, it will consider that it is parsing a cookie-av value 511// (cookie attribute-value). 512// 513// It returns the parsed cookie value, a boolean indicating whether the 514// parsing was successful, and a boolean indicating whether the parsed 515// value was enclosed in double quotes. 516func parseCookieValue(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) { 517 // Strip the quotes, if present. 518 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { 519 raw = raw[1 : len(raw)-1] 520 quoted = true 521 } 522 for i := 0; i < len(raw); i++ { 523 if !validCookieValueByte(raw[i]) { 524 return "", quoted, false 525 } 526 } 527 return raw, quoted, true 528} 529 530func isCookieNameValid(raw string) bool { 531 if raw == "" { 532 return false 533 } 534 return strings.IndexFunc(raw, isNotToken) < 0 535} 536