cookie.mx raw

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