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 }