github.com/cloudwego/hertz@v0.9.3/pkg/protocol/cookie.go (about)

     1  /*
     2   * Copyright 2022 CloudWeGo Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   * The MIT License (MIT)
    17   *
    18   * Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors
    19   *
    20   * Permission is hereby granted, free of charge, to any person obtaining a copy
    21   * of this software and associated documentation files (the "Software"), to deal
    22   * in the Software without restriction, including without limitation the rights
    23   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    24   * copies of the Software, and to permit persons to whom the Software is
    25   * furnished to do so, subject to the following conditions:
    26   *
    27   * The above copyright notice and this permission notice shall be included in
    28   * all copies or substantial portions of the Software.
    29   *
    30   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    31   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    32   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    33   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    34   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    35   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    36   * THE SOFTWARE.
    37   *
    38   * This file may have been modified by CloudWeGo authors. All CloudWeGo
    39   * Modifications are Copyright 2022 CloudWeGo Authors.
    40   */
    41  
    42  package protocol
    43  
    44  import (
    45  	"bytes"
    46  	"sync"
    47  	"time"
    48  
    49  	"github.com/cloudwego/hertz/internal/bytesconv"
    50  	"github.com/cloudwego/hertz/internal/bytestr"
    51  	"github.com/cloudwego/hertz/internal/nocopy"
    52  	"github.com/cloudwego/hertz/pkg/common/errors"
    53  	"github.com/cloudwego/hertz/pkg/common/hlog"
    54  	"github.com/cloudwego/hertz/pkg/common/utils"
    55  )
    56  
    57  const (
    58  	// CookieSameSiteDisabled removes the SameSite flag
    59  	CookieSameSiteDisabled CookieSameSite = iota
    60  	// CookieSameSiteDefaultMode sets the SameSite flag
    61  	CookieSameSiteDefaultMode
    62  	// CookieSameSiteLaxMode sets the SameSite flag with the "Lax" parameter
    63  	CookieSameSiteLaxMode
    64  	// CookieSameSiteStrictMode sets the SameSite flag with the "Strict" parameter
    65  	CookieSameSiteStrictMode
    66  	// CookieSameSiteNoneMode sets the SameSite flag with the "None" parameter
    67  	// see https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
    68  	// third-party cookies are phasing out, use Partitioned cookies instead
    69  	// see https://developers.google.com/privacy-sandbox/3pcd
    70  	CookieSameSiteNoneMode
    71  )
    72  
    73  var zeroTime time.Time
    74  
    75  var (
    76  	errNoCookies = errors.NewPublic("no cookies found")
    77  
    78  	// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
    79  	CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
    80  
    81  	// CookieExpireUnlimited indicates that the cookie doesn't expire.
    82  	CookieExpireUnlimited = zeroTime
    83  )
    84  
    85  // CookieSameSite is an enum for the mode in which the SameSite flag should be set for the given cookie.
    86  // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
    87  type CookieSameSite int
    88  
    89  // Cookie represents HTTP response cookie.
    90  //
    91  // Do not copy Cookie objects. Create new object and use CopyTo instead.
    92  //
    93  // Cookie instance MUST NOT be used from concurrently running goroutines.
    94  type Cookie struct {
    95  	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
    96  
    97  	key    []byte
    98  	value  []byte
    99  	expire time.Time
   100  	maxAge int
   101  	domain []byte
   102  	path   []byte
   103  
   104  	httpOnly bool
   105  	secure   bool
   106  	// A partitioned third-party cookie is tied to the top-level site
   107  	// where it's initially set and cannot be accessed from elsewhere.
   108  	partitioned bool
   109  	sameSite    CookieSameSite
   110  
   111  	bufKV argsKV
   112  	buf   []byte
   113  }
   114  
   115  var cookiePool = &sync.Pool{
   116  	New: func() interface{} {
   117  		return &Cookie{}
   118  	},
   119  }
   120  
   121  // AcquireCookie returns an empty Cookie object from the pool.
   122  //
   123  // The returned object may be returned back to the pool with ReleaseCookie.
   124  // This allows reducing GC load.
   125  func AcquireCookie() *Cookie {
   126  	return cookiePool.Get().(*Cookie)
   127  }
   128  
   129  // ReleaseCookie returns the Cookie object acquired with AcquireCookie back
   130  // to the pool.
   131  //
   132  // Do not access released Cookie object, otherwise data races may occur.
   133  func ReleaseCookie(c *Cookie) {
   134  	c.Reset()
   135  	cookiePool.Put(c)
   136  }
   137  
   138  // SetDomain sets cookie domain.
   139  func (c *Cookie) SetDomain(domain string) {
   140  	c.domain = append(c.domain[:0], domain...)
   141  }
   142  
   143  // SetPath sets cookie path.
   144  func (c *Cookie) SetPath(path string) {
   145  	c.buf = append(c.buf[:0], path...)
   146  	c.path = normalizePath(c.path, c.buf)
   147  }
   148  
   149  // SetPathBytes sets cookie path.
   150  func (c *Cookie) SetPathBytes(path []byte) {
   151  	c.buf = append(c.buf[:0], path...)
   152  	c.path = normalizePath(c.path, c.buf)
   153  }
   154  
   155  // SetExpire sets cookie expiration time.
   156  //
   157  // Set expiration time to CookieExpireDelete for expiring (deleting)
   158  // the cookie on the client.
   159  //
   160  // By default cookie lifetime is limited by browser session.
   161  func (c *Cookie) SetExpire(expire time.Time) {
   162  	c.expire = expire
   163  }
   164  
   165  // SetKey sets cookie name.
   166  func (c *Cookie) SetKey(key string) {
   167  	c.key = append(c.key[:0], key...)
   168  }
   169  
   170  // SetKeyBytes sets cookie name.
   171  func (c *Cookie) SetKeyBytes(key []byte) {
   172  	c.key = append(c.key[:0], key...)
   173  }
   174  
   175  // SetValue sets cookie value.
   176  func (c *Cookie) SetValue(value string) {
   177  	warnIfInvalid(bytesconv.S2b(value))
   178  	c.value = append(c.value[:0], value...)
   179  }
   180  
   181  // SetValueBytes sets cookie value.
   182  func (c *Cookie) SetValueBytes(value []byte) {
   183  	warnIfInvalid(value)
   184  	c.value = append(c.value[:0], value...)
   185  }
   186  
   187  // AppendBytes appends cookie representation to dst and returns
   188  // the extended dst.
   189  func (c *Cookie) AppendBytes(dst []byte) []byte {
   190  	if len(c.key) > 0 {
   191  		dst = append(dst, c.key...)
   192  		dst = append(dst, '=')
   193  	}
   194  	dst = append(dst, c.value...)
   195  
   196  	if c.maxAge > 0 {
   197  		dst = append(dst, ';', ' ')
   198  		dst = append(dst, bytestr.StrCookieMaxAge...)
   199  		dst = append(dst, '=')
   200  		dst = bytesconv.AppendUint(dst, c.maxAge)
   201  	} else if !c.expire.IsZero() {
   202  		c.bufKV.value = bytesconv.AppendHTTPDate(c.bufKV.value[:0], c.expire)
   203  		dst = appendCookiePart(dst, bytestr.StrCookieExpires, c.bufKV.value)
   204  	}
   205  	if len(c.domain) > 0 {
   206  		dst = appendCookiePart(dst, bytestr.StrCookieDomain, c.domain)
   207  	}
   208  	if len(c.path) > 0 {
   209  		dst = appendCookiePart(dst, bytestr.StrCookiePath, c.path)
   210  	}
   211  	if c.httpOnly {
   212  		dst = append(dst, ';', ' ')
   213  		dst = append(dst, bytestr.StrCookieHTTPOnly...)
   214  	}
   215  	if c.secure {
   216  		dst = append(dst, ';', ' ')
   217  		dst = append(dst, bytestr.StrCookieSecure...)
   218  	}
   219  	switch c.sameSite {
   220  	case CookieSameSiteDefaultMode:
   221  		dst = append(dst, ';', ' ')
   222  		dst = append(dst, bytestr.StrCookieSameSite...)
   223  	case CookieSameSiteLaxMode:
   224  		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteLax)
   225  	case CookieSameSiteStrictMode:
   226  		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteStrict)
   227  	case CookieSameSiteNoneMode:
   228  		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteNone)
   229  	}
   230  
   231  	if c.partitioned {
   232  		dst = append(dst, ';', ' ')
   233  		dst = append(dst, bytestr.StrCookiePartitioned...)
   234  	}
   235  
   236  	return dst
   237  }
   238  
   239  func appendCookiePart(dst, key, value []byte) []byte {
   240  	dst = append(dst, ';', ' ')
   241  	dst = append(dst, key...)
   242  	dst = append(dst, '=')
   243  	return append(dst, value...)
   244  }
   245  
   246  func appendRequestCookieBytes(dst []byte, cookies []argsKV) []byte {
   247  	for i, n := 0, len(cookies); i < n; i++ {
   248  		kv := &cookies[i]
   249  		if len(kv.key) > 0 {
   250  			dst = append(dst, kv.key...)
   251  			dst = append(dst, '=')
   252  		}
   253  		dst = append(dst, kv.value...)
   254  		if i+1 < n {
   255  			dst = append(dst, ';', ' ')
   256  		}
   257  	}
   258  	return dst
   259  }
   260  
   261  // For Response we can not use the above function as response cookies
   262  // already contain the key= in the value.
   263  func appendResponseCookieBytes(dst []byte, cookies []argsKV) []byte {
   264  	for i, n := 0, len(cookies); i < n; i++ {
   265  		kv := &cookies[i]
   266  		dst = append(dst, kv.value...)
   267  		if i+1 < n {
   268  			dst = append(dst, ';', ' ')
   269  		}
   270  	}
   271  	return dst
   272  }
   273  
   274  type cookieScanner struct {
   275  	b []byte
   276  }
   277  
   278  func parseRequestCookies(cookies []argsKV, src []byte) []argsKV {
   279  	var s cookieScanner
   280  	s.b = src
   281  	var kv *argsKV
   282  	cookies, kv = allocArg(cookies)
   283  	for s.next(kv) {
   284  		if len(kv.key) > 0 || len(kv.value) > 0 {
   285  			cookies, kv = allocArg(cookies)
   286  		}
   287  	}
   288  	return releaseArg(cookies)
   289  }
   290  
   291  func (s *cookieScanner) next(kv *argsKV) bool {
   292  	b := s.b
   293  	if len(b) == 0 {
   294  		return false
   295  	}
   296  
   297  	isKey := true
   298  	k := 0
   299  	for i, c := range b {
   300  		switch c {
   301  		case '=':
   302  			if isKey {
   303  				isKey = false
   304  				kv.key = decodeCookieArg(kv.key, b[:i], false)
   305  				k = i + 1
   306  			}
   307  		case ';':
   308  			if isKey {
   309  				kv.key = kv.key[:0]
   310  			}
   311  			kv.value = decodeCookieArg(kv.value, b[k:i], true)
   312  			s.b = b[i+1:]
   313  			return true
   314  		}
   315  	}
   316  
   317  	if isKey {
   318  		kv.key = kv.key[:0]
   319  	}
   320  	kv.value = decodeCookieArg(kv.value, b[k:], true)
   321  	s.b = b[len(b):]
   322  	return true
   323  }
   324  
   325  // Key returns cookie name.
   326  //
   327  // The returned value is valid until the next Cookie modification method call.
   328  func (c *Cookie) Key() []byte {
   329  	return c.key
   330  }
   331  
   332  // Cookie returns cookie representation.
   333  //
   334  // The returned value is valid until the next call to Cookie methods.
   335  func (c *Cookie) Cookie() []byte {
   336  	c.buf = c.AppendBytes(c.buf[:0])
   337  	return c.buf
   338  }
   339  
   340  // Reset clears the cookie.
   341  func (c *Cookie) Reset() {
   342  	c.key = c.key[:0]
   343  	c.value = c.value[:0]
   344  	c.expire = zeroTime
   345  	c.maxAge = 0
   346  	c.domain = c.domain[:0]
   347  	c.path = c.path[:0]
   348  	c.httpOnly = false
   349  	c.secure = false
   350  	c.sameSite = CookieSameSiteDisabled
   351  	c.partitioned = false
   352  }
   353  
   354  // Value returns cookie value.
   355  //
   356  // The returned value is valid until the next Cookie modification method call.
   357  func (c *Cookie) Value() []byte {
   358  	return c.value
   359  }
   360  
   361  // Parse parses Set-Cookie header.
   362  func (c *Cookie) Parse(src string) error {
   363  	c.buf = append(c.buf[:0], src...)
   364  	return c.ParseBytes(c.buf)
   365  }
   366  
   367  // ParseBytes parses Set-Cookie header.
   368  func (c *Cookie) ParseBytes(src []byte) error {
   369  	c.Reset()
   370  
   371  	var s cookieScanner
   372  	s.b = src
   373  
   374  	kv := &c.bufKV
   375  	if !s.next(kv) {
   376  		return errNoCookies
   377  	}
   378  
   379  	c.key = append(c.key[:0], kv.key...)
   380  	c.value = append(c.value[:0], kv.value...)
   381  
   382  	for s.next(kv) {
   383  		if len(kv.key) != 0 {
   384  			// Case-insensitive switch on first char
   385  			switch kv.key[0] | 0x20 {
   386  			case 'm':
   387  				if utils.CaseInsensitiveCompare(bytestr.StrCookieMaxAge, kv.key) {
   388  					maxAge, err := bytesconv.ParseUint(kv.value)
   389  					if err != nil {
   390  						return err
   391  					}
   392  					c.maxAge = maxAge
   393  				}
   394  
   395  			case 'e': // "expires"
   396  				if utils.CaseInsensitiveCompare(bytestr.StrCookieExpires, kv.key) {
   397  					v := bytesconv.B2s(kv.value)
   398  					// Try the same two formats as net/http
   399  					// See: https://github.com/golang/go/blob/00379be17e63a5b75b3237819392d2dc3b313a27/src/net/http/cookie.go#L133-L135
   400  					exptime, err := time.ParseInLocation(time.RFC1123, v, time.UTC)
   401  					if err != nil {
   402  						exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", v)
   403  						if err != nil {
   404  							return err
   405  						}
   406  					}
   407  					c.expire = exptime
   408  				}
   409  
   410  			case 'd': // "domain"
   411  				if utils.CaseInsensitiveCompare(bytestr.StrCookieDomain, kv.key) {
   412  					c.domain = append(c.domain[:0], kv.value...)
   413  				}
   414  
   415  			case 'p': // "path"
   416  				if utils.CaseInsensitiveCompare(bytestr.StrCookiePath, kv.key) {
   417  					c.path = append(c.path[:0], kv.value...)
   418  				}
   419  
   420  			case 's': // "samesite"
   421  				if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSite, kv.key) {
   422  					// Case-insensitive switch on first char
   423  					switch kv.value[0] | 0x20 {
   424  					case 'l': // "lax"
   425  						if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSiteLax, kv.value) {
   426  							c.sameSite = CookieSameSiteLaxMode
   427  						}
   428  					case 's': // "strict"
   429  						if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSiteStrict, kv.value) {
   430  							c.sameSite = CookieSameSiteStrictMode
   431  						}
   432  					case 'n': // "none"
   433  						if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSiteNone, kv.value) {
   434  							c.sameSite = CookieSameSiteNoneMode
   435  						}
   436  					}
   437  				}
   438  			}
   439  		} else if len(kv.value) != 0 {
   440  			// Case-insensitive switch on first char
   441  			switch kv.value[0] | 0x20 {
   442  			case 'h': // "httponly"
   443  				if utils.CaseInsensitiveCompare(bytestr.StrCookieHTTPOnly, kv.value) {
   444  					c.httpOnly = true
   445  				}
   446  
   447  			case 's': // "secure"
   448  				if utils.CaseInsensitiveCompare(bytestr.StrCookieSecure, kv.value) {
   449  					c.secure = true
   450  				} else if utils.CaseInsensitiveCompare(bytestr.StrCookieSameSite, kv.value) {
   451  					c.sameSite = CookieSameSiteDefaultMode
   452  				}
   453  			case 'p': // "partitioned"
   454  				if utils.CaseInsensitiveCompare(bytestr.StrCookiePartitioned, kv.value) {
   455  					c.partitioned = true
   456  				}
   457  			}
   458  		} // else empty or no match
   459  	}
   460  
   461  	return nil
   462  }
   463  
   464  // MaxAge returns the seconds until the cookie is meant to expire or 0
   465  // if no max age.
   466  func (c *Cookie) MaxAge() int {
   467  	return c.maxAge
   468  }
   469  
   470  // SetMaxAge sets cookie expiration time based on seconds. This takes precedence
   471  // over any absolute expiry set on the cookie
   472  //
   473  // Set max age to 0 to unset
   474  func (c *Cookie) SetMaxAge(seconds int) {
   475  	c.maxAge = seconds
   476  }
   477  
   478  // Expire returns cookie expiration time.
   479  //
   480  // CookieExpireUnlimited is returned if cookie doesn't expire
   481  func (c *Cookie) Expire() time.Time {
   482  	expire := c.expire
   483  	if expire.IsZero() {
   484  		expire = CookieExpireUnlimited
   485  	}
   486  	return expire
   487  }
   488  
   489  // Domain returns cookie domain.
   490  //
   491  // The returned domain is valid until the next Cookie modification method call.
   492  func (c *Cookie) Domain() []byte {
   493  	return c.domain
   494  }
   495  
   496  // Path returns cookie path.
   497  func (c *Cookie) Path() []byte {
   498  	return c.path
   499  }
   500  
   501  // Secure returns true if the cookie is secure.
   502  func (c *Cookie) Secure() bool {
   503  	return c.secure
   504  }
   505  
   506  // SetSecure sets cookie's secure flag to the given value.
   507  func (c *Cookie) SetSecure(secure bool) {
   508  	c.secure = secure
   509  }
   510  
   511  // SameSite returns the SameSite mode.
   512  func (c *Cookie) SameSite() CookieSameSite {
   513  	return c.sameSite
   514  }
   515  
   516  // Partitioned returns if cookie is partitioned.
   517  func (c *Cookie) Partitioned() bool {
   518  	return c.partitioned
   519  }
   520  
   521  // SetSameSite sets the cookie's SameSite flag to the given value.
   522  // set value CookieSameSiteNoneMode will set Secure to true also to avoid browser rejection
   523  func (c *Cookie) SetSameSite(mode CookieSameSite) {
   524  	c.sameSite = mode
   525  	if mode == CookieSameSiteNoneMode {
   526  		c.SetSecure(true)
   527  	}
   528  }
   529  
   530  // HTTPOnly returns true if the cookie is http only.
   531  func (c *Cookie) HTTPOnly() bool {
   532  	return c.httpOnly
   533  }
   534  
   535  // SetHTTPOnly sets cookie's httpOnly flag to the given value.
   536  func (c *Cookie) SetHTTPOnly(httpOnly bool) {
   537  	c.httpOnly = httpOnly
   538  }
   539  
   540  // SetPartitioned sets cookie as partitioned. Setting Partitioned to true will also set Secure.
   541  func (c *Cookie) SetPartitioned(partitioned bool) {
   542  	c.partitioned = partitioned
   543  	if partitioned {
   544  		c.SetSecure(true)
   545  	}
   546  }
   547  
   548  // String returns cookie representation.
   549  func (c *Cookie) String() string {
   550  	return string(c.Cookie())
   551  }
   552  
   553  func decodeCookieArg(dst, src []byte, skipQuotes bool) []byte {
   554  	for len(src) > 0 && src[0] == ' ' {
   555  		src = src[1:]
   556  	}
   557  	for len(src) > 0 && src[len(src)-1] == ' ' {
   558  		src = src[:len(src)-1]
   559  	}
   560  	if skipQuotes {
   561  		if len(src) > 1 && src[0] == '"' && src[len(src)-1] == '"' {
   562  			src = src[1 : len(src)-1]
   563  		}
   564  	}
   565  	return append(dst[:0], src...)
   566  }
   567  
   568  func getCookieKey(dst, src []byte) []byte {
   569  	n := bytes.IndexByte(src, '=')
   570  	if n >= 0 {
   571  		src = src[:n]
   572  	}
   573  	return decodeCookieArg(dst, src, false)
   574  }
   575  
   576  func warnIfInvalid(value []byte) bool {
   577  	for i := range value {
   578  		if bytesconv.ValidCookieValueTable[value[i]] == 0 {
   579  			hlog.SystemLogger().Warnf("Invalid byte %q in Cookie.Value, "+
   580  				"it may cause compatibility problems with user agents", value[i])
   581  			return false
   582  		}
   583  	}
   584  	return true
   585  }