Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import base64 

2import binascii 

3import hashlib 

4import hmac 

5import json 

6from datetime import ( 

7 date, 

8 datetime, 

9 timedelta, 

10 ) 

11import re 

12import string 

13import time 

14import warnings 

15 

16from webob.compat import ( 

17 MutableMapping, 

18 PY2, 

19 text_type, 

20 bytes_, 

21 text_, 

22 native_, 

23 string_types, 

24 ) 

25 

26from webob.util import strings_differ 

27 

28__all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer', 

29 'JSONSerializer', 'Base64Serializer', 'make_cookie'] 

30 

31_marker = object() 

32 

33# Module flag to handle validation of SameSite attributes 

34# See the documentation for ``make_cookie`` for more information. 

35SAMESITE_VALIDATION = True 

36 

37 

38class RequestCookies(MutableMapping): 

39 

40 _cache_key = 'webob._parsed_cookies' 

41 

42 def __init__(self, environ): 

43 self._environ = environ 

44 

45 @property 

46 def _cache(self): 

47 env = self._environ 

48 header = env.get('HTTP_COOKIE', '') 

49 cache, cache_header = env.get(self._cache_key, ({}, None)) 

50 if cache_header == header: 

51 return cache 

52 d = lambda b: b.decode('utf8') 

53 cache = dict((d(k), d(v)) for k,v in parse_cookie(header)) 

54 env[self._cache_key] = (cache, header) 

55 return cache 

56 

57 def _mutate_header(self, name, value): 

58 header = self._environ.get('HTTP_COOKIE') 

59 had_header = header is not None 

60 header = header or '' 

61 if not PY2: 

62 header = header.encode('latin-1') 

63 bytes_name = bytes_(name, 'ascii') 

64 if value is None: 

65 replacement = None 

66 else: 

67 bytes_val = _value_quote(bytes_(value, 'utf-8')) 

68 replacement = bytes_name + b'=' + bytes_val 

69 matches = _rx_cookie.finditer(header) 

70 found = False 

71 for match in matches: 

72 start, end = match.span() 

73 match_name = match.group(1) 

74 if match_name == bytes_name: 

75 found = True 

76 if replacement is None: # remove value 

77 header = header[:start].rstrip(b' ;') + header[end:] 

78 else: # replace value 

79 header = header[:start] + replacement + header[end:] 

80 break 

81 else: 

82 if replacement is not None: 

83 if header: 

84 header += b'; ' + replacement 

85 else: 

86 header = replacement 

87 

88 if header: 

89 self._environ['HTTP_COOKIE'] = native_(header, 'latin-1') 

90 elif had_header: 

91 self._environ['HTTP_COOKIE'] = '' 

92 

93 return found 

94 

95 def _valid_cookie_name(self, name): 

96 if not isinstance(name, string_types): 

97 raise TypeError(name, 'cookie name must be a string') 

98 if not isinstance(name, text_type): 

99 name = text_(name, 'utf-8') 

100 try: 

101 bytes_cookie_name = bytes_(name, 'ascii') 

102 except UnicodeEncodeError: 

103 raise TypeError('cookie name must be encodable to ascii') 

104 if not _valid_cookie_name(bytes_cookie_name): 

105 raise TypeError('cookie name must be valid according to RFC 6265') 

106 return name 

107 

108 def __setitem__(self, name, value): 

109 name = self._valid_cookie_name(name) 

110 if not isinstance(value, string_types): 

111 raise ValueError(value, 'cookie value must be a string') 

112 if not isinstance(value, text_type): 

113 try: 

114 value = text_(value, 'utf-8') 

115 except UnicodeDecodeError: 

116 raise ValueError( 

117 value, 'cookie value must be utf-8 binary or unicode') 

118 self._mutate_header(name, value) 

119 

120 def __getitem__(self, name): 

121 return self._cache[name] 

122 

123 def get(self, name, default=None): 

124 return self._cache.get(name, default) 

125 

126 def __delitem__(self, name): 

127 name = self._valid_cookie_name(name) 

128 found = self._mutate_header(name, None) 

129 if not found: 

130 raise KeyError(name) 

131 

132 def keys(self): 

133 return self._cache.keys() 

134 

135 def values(self): 

136 return self._cache.values() 

137 

138 def items(self): 

139 return self._cache.items() 

140 

141 if PY2: 

142 def iterkeys(self): 

143 return self._cache.iterkeys() 

144 

145 def itervalues(self): 

146 return self._cache.itervalues() 

147 

148 def iteritems(self): 

149 return self._cache.iteritems() 

150 

151 def __contains__(self, name): 

152 return name in self._cache 

153 

154 def __iter__(self): 

155 return self._cache.__iter__() 

156 

157 def __len__(self): 

158 return len(self._cache) 

159 

160 def clear(self): 

161 self._environ['HTTP_COOKIE'] = '' 

162 

163 def __repr__(self): 

164 return '<RequestCookies (dict-like) with values %r>' % (self._cache,) 

165 

166 

167class Cookie(dict): 

168 def __init__(self, input=None): 

169 if input: 

170 self.load(input) 

171 

172 def load(self, data): 

173 morsel = {} 

174 for key, val in _parse_cookie(data): 

175 if key.lower() in _c_keys: 

176 morsel[key] = val 

177 else: 

178 morsel = self.add(key, val) 

179 

180 def add(self, key, val): 

181 if not isinstance(key, bytes): 

182 key = key.encode('ascii', 'replace') 

183 if not _valid_cookie_name(key): 

184 return {} 

185 r = Morsel(key, val) 

186 dict.__setitem__(self, key, r) 

187 return r 

188 __setitem__ = add 

189 

190 def serialize(self, full=True): 

191 return '; '.join(m.serialize(full) for m in self.values()) 

192 

193 def values(self): 

194 return [m for _, m in sorted(self.items())] 

195 

196 __str__ = serialize 

197 

198 def __repr__(self): 

199 return '<%s: [%s]>' % (self.__class__.__name__, 

200 ', '.join(map(repr, self.values()))) 

201 

202 

203def _parse_cookie(data): 

204 if not PY2: 

205 data = data.encode('latin-1') 

206 for key, val in _rx_cookie.findall(data): 

207 yield key, _unquote(val) 

208 

209def parse_cookie(data): 

210 """ 

211 Parse cookies ignoring anything except names and values 

212 """ 

213 return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k)) 

214 

215 

216def cookie_property(key, serialize=lambda v: v): 

217 def fset(self, v): 

218 self[key] = serialize(v) 

219 return property(lambda self: self[key], fset) 

220 

221def serialize_max_age(v): 

222 if isinstance(v, timedelta): 

223 v = str(v.seconds + v.days*24*60*60) 

224 elif isinstance(v, int): 

225 v = str(v) 

226 return bytes_(v) 

227 

228def serialize_cookie_date(v): 

229 if v is None: 

230 return None 

231 elif isinstance(v, bytes): 

232 return v 

233 elif isinstance(v, text_type): 

234 return v.encode('ascii') 

235 elif isinstance(v, int): 

236 v = timedelta(seconds=v) 

237 if isinstance(v, timedelta): 

238 v = datetime.utcnow() + v 

239 if isinstance(v, (datetime, date)): 

240 v = v.timetuple() 

241 r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v) 

242 return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii') 

243 

244 

245def serialize_samesite(v): 

246 v = bytes_(v) 

247 

248 if SAMESITE_VALIDATION: 

249 if v.lower() not in (b"strict", b"lax", b"none"): 

250 raise ValueError("SameSite must be 'strict', 'lax', or 'none'") 

251 

252 return v 

253 

254 

255class Morsel(dict): 

256 __slots__ = ('name', 'value') 

257 def __init__(self, name, value): 

258 self.name = bytes_(name, encoding='ascii') 

259 self.value = bytes_(value, encoding='ascii') 

260 assert _valid_cookie_name(self.name) 

261 self.update(dict.fromkeys(_c_keys, None)) 

262 

263 path = cookie_property(b'path') 

264 domain = cookie_property(b'domain') 

265 comment = cookie_property(b'comment') 

266 expires = cookie_property(b'expires', serialize_cookie_date) 

267 max_age = cookie_property(b'max-age', serialize_max_age) 

268 httponly = cookie_property(b'httponly', bool) 

269 secure = cookie_property(b'secure', bool) 

270 samesite = cookie_property(b'samesite', serialize_samesite) 

271 

272 def __setitem__(self, k, v): 

273 k = bytes_(k.lower(), 'ascii') 

274 if k in _c_keys: 

275 dict.__setitem__(self, k, v) 

276 

277 def serialize(self, full=True): 

278 result = [] 

279 add = result.append 

280 add(self.name + b'=' + _value_quote(self.value)) 

281 if full: 

282 for k in _c_valkeys: 

283 v = self[k] 

284 if v: 

285 info = _c_renames[k] 

286 name = info['name'] 

287 quoter = info['quoter'] 

288 add(name + b'=' + quoter(v)) 

289 expires = self[b'expires'] 

290 if expires: 

291 add(b'expires=' + expires) 

292 if self.secure: 

293 add(b'secure') 

294 if self.httponly: 

295 add(b'HttpOnly') 

296 if self.samesite: 

297 if not self.secure and self.samesite.lower() == b"none": 

298 raise ValueError( 

299 "Incompatible cookie attributes: " 

300 "when the samesite equals 'none', then the secure must be True" 

301 ) 

302 add(b"SameSite=" + self.samesite) 

303 

304 return native_(b"; ".join(result), "ascii") 

305 

306 __str__ = serialize 

307 

308 def __repr__(self): 

309 return '<%s: %s=%r>' % (self.__class__.__name__, 

310 native_(self.name), 

311 native_(self.value) 

312 ) 

313 

314# 

315# parsing 

316# 

317 

318 

319_re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string 

320_legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'" 

321_re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars) 

322_re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" 

323_re_cookie_str_key = r"(%s+?)" % _re_legal_char 

324_re_cookie_str_equal = r"\s*=\s*" 

325_re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char 

326_re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val, 

327 _re_unquoted_val) 

328_re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val 

329 

330_rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii')) 

331_rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii')) 

332 

333_bchr = chr if PY2 else (lambda i: bytes([i])) 

334_ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i)) 

335 for i in range(256) 

336) 

337_ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values())) 

338 

339_b_dollar_sign = '$' if PY2 else ord('$') 

340_b_quote_mark = '"' if PY2 else ord('"') 

341 

342def _unquote(v): 

343 #assert isinstance(v, bytes) 

344 if v and v[0] == v[-1] == _b_quote_mark: 

345 v = v[1:-1] 

346 return _rx_unquote.sub(_ch_unquote, v) 

347 

348def _ch_unquote(m): 

349 return _ch_unquote_map[m.group(1)] 

350 

351 

352# 

353# serializing 

354# 

355 

356# these chars can be in cookie value see 

357# http://tools.ietf.org/html/rfc6265#section-4.1.1 and 

358# https://github.com/Pylons/webob/pull/104#issuecomment-28044314 

359# 

360# ! (0x21), "#$%&'()*+" (0x25-0x2B), "-./0123456789:" (0x2D-0x3A), 

361# "<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" (0x3C-0x5B), 

362# "]^_`abcdefghijklmnopqrstuvwxyz{|}~" (0x5D-0x7E) 

363 

364_allowed_special_chars = "!#$%&'()*+-./:<=>?@[]^_`{|}~" 

365_allowed_cookie_chars = (string.ascii_letters + string.digits + 

366 _allowed_special_chars) 

367_allowed_cookie_bytes = bytes_(_allowed_cookie_chars) 

368 

369# these are the characters accepted in cookie *names* 

370# From http://tools.ietf.org/html/rfc2616#section-2.2: 

371# token = 1*<any CHAR except CTLs or separators> 

372# separators = "(" | ")" | "<" | ">" | "@" 

373# | "," | ";" | ":" | "\" | <"> 

374# | "/" | "[" | "]" | "?" | "=" 

375# | "{" | "}" | SP | HT 

376# 

377# CTL = <any US-ASCII control character 

378# (octets 0 - 31) and DEL (127)> 

379# 

380_valid_token_chars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~" 

381_valid_token_bytes = bytes_(_valid_token_chars) 

382 

383# this is a map used to escape the values 

384 

385_escape_noop_chars = _allowed_cookie_chars + ' ' 

386_escape_map = dict((chr(i), '\\%03o' % i) for i in range(256)) 

387_escape_map.update(zip(_escape_noop_chars, _escape_noop_chars)) 

388if not PY2: 

389 # convert to {int -> bytes} 

390 _escape_map = dict( 

391 (ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items() 

392 ) 

393_escape_char = _escape_map.__getitem__ 

394 

395weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') 

396months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 

397 'Oct', 'Nov', 'Dec') 

398 

399 

400# This is temporary, until we can remove this from _value_quote 

401_should_raise = None 

402 

403def __warn_or_raise(text, warn_class, to_raise, raise_reason): 

404 if _should_raise: 

405 raise to_raise(raise_reason) 

406 

407 else: 

408 warnings.warn(text, warn_class, stacklevel=2) 

409 

410 

411def _value_quote(v): 

412 # This looks scary, but is simple. We remove all valid characters from the 

413 # string, if we end up with leftovers (string is longer than 0, we have 

414 # invalid characters in our value) 

415 

416 leftovers = v.translate(None, _allowed_cookie_bytes) 

417 if leftovers: 

418 __warn_or_raise( 

419 "Cookie value contains invalid bytes: (%r). Future versions " 

420 "will raise ValueError upon encountering invalid bytes." % 

421 (leftovers,), 

422 RuntimeWarning, ValueError, 'Invalid characters in cookie value' 

423 ) 

424 #raise ValueError('Invalid characters in cookie value') 

425 return b'"' + b''.join(map(_escape_char, v)) + b'"' 

426 

427 return v 

428 

429def _valid_cookie_name(key): 

430 return isinstance(key, bytes) and not ( 

431 key.translate(None, _valid_token_bytes) 

432 # Not explicitly required by RFC6265, may consider removing later: 

433 or key[0] == _b_dollar_sign 

434 or key.lower() in _c_keys 

435 ) 

436 

437def _path_quote(v): 

438 return b''.join(map(_escape_char, v)) 

439 

440_domain_quote = _path_quote 

441_max_age_quote = _path_quote 

442 

443_c_renames = { 

444 b"path" : {'name':b"Path", 'quoter':_path_quote}, 

445 b"comment" : {'name':b"Comment", 'quoter':_value_quote}, 

446 b"domain" : {'name':b"Domain", 'quoter':_domain_quote}, 

447 b"max-age" : {'name':b"Max-Age", 'quoter':_max_age_quote}, 

448 } 

449_c_valkeys = sorted(_c_renames) 

450_c_keys = set(_c_renames) 

451_c_keys.update([b'expires', b'secure', b'httponly', b'samesite']) 

452 

453 

454def make_cookie(name, value, max_age=None, path='/', domain=None, 

455 secure=False, httponly=False, comment=None, samesite=None): 

456 """ 

457 Generate a cookie value. 

458 

459 ``name`` 

460 The name of the cookie. 

461 

462 ``value`` 

463 The ``value`` of the cookie. If it is ``None``, it will generate a cookie 

464 value with an expiration date in the past. 

465 

466 ``max_age`` 

467 The maximum age of the cookie used for sessioning (in seconds). 

468 Default: ``None`` (browser scope). 

469 

470 ``path`` 

471 The path used for the session cookie. Default: ``/``. 

472 

473 ``domain`` 

474 The domain used for the session cookie. Default: ``None`` (no domain). 

475 

476 ``secure`` 

477 The 'secure' flag of the session cookie. Default: ``False``. 

478 

479 ``httponly`` 

480 Hide the cookie from JavaScript by setting the 'HttpOnly' flag of the 

481 session cookie. Default: ``False``. 

482 

483 ``comment`` 

484 Set a comment on the cookie. Default: ``None`` 

485 

486 ``samesite`` 

487 The 'SameSite' attribute of the cookie, can be either ``"strict"``, 

488 ``"lax"``, ``"none"``, or ``None``. By default, WebOb will validate the 

489 value to ensure it conforms to the allowable options in the various draft 

490 RFC's that exist. 

491 

492 To disable this check and send headers that are experimental or introduced 

493 in a future RFC, set the module flag ``SAMESITE_VALIDATION`` to a 

494 false value like: 

495 

496 .. code:: 

497 

498 import webob.cookies 

499 webob.cookies.SAMESITE_VALIDATION = False 

500 

501 ck = webob.cookies.make_cookie(cookie_name, value, samesite='future') 

502 

503 .. danger:: 

504 

505 This feature has known compatibility issues with various user agents, 

506 and is not yet an accepted RFC. It is therefore considered 

507 experimental and subject to change. 

508 

509 For more information please see :ref:`Experimental: SameSite Cookies 

510 <samesiteexp>` 

511 """ 

512 

513 # We are deleting the cookie, override max_age and expires 

514 if value is None: 

515 value = b'' 

516 # Note that the max-age value of zero is technically contraspec; 

517 # RFC6265 says that max-age cannot be zero. However, all browsers 

518 # appear to support this to mean "delete immediately". 

519 # http://www.timwilson.id.au/news-three-critical-problems-with-rfc6265.html 

520 max_age = 0 

521 expires = 'Wed, 31-Dec-97 23:59:59 GMT' 

522 

523 # Convert max_age to seconds 

524 elif isinstance(max_age, timedelta): 

525 max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds 

526 expires = max_age 

527 elif max_age is not None: 

528 try: 

529 max_age = int(max_age) 

530 except ValueError: 

531 raise ValueError('max_age should be an integer. Amount of seconds until expiration.') 

532 

533 expires = max_age 

534 else: 

535 expires = None 

536 

537 morsel = Morsel(name, value) 

538 

539 if domain is not None: 

540 morsel.domain = bytes_(domain) 

541 if path is not None: 

542 morsel.path = bytes_(path) 

543 if httponly: 

544 morsel.httponly = True 

545 if secure: 

546 morsel.secure = True 

547 if max_age is not None: 

548 morsel.max_age = max_age 

549 if expires is not None: 

550 morsel.expires = expires 

551 if comment is not None: 

552 morsel.comment = bytes_(comment) 

553 if samesite is not None: 

554 morsel.samesite = samesite 

555 return morsel.serialize() 

556 

557class JSONSerializer(object): 

558 """ A serializer which uses `json.dumps`` and ``json.loads``""" 

559 def dumps(self, appstruct): 

560 return bytes_(json.dumps(appstruct), encoding='utf-8') 

561 

562 def loads(self, bstruct): 

563 # NB: json.loads raises ValueError if no json object can be decoded 

564 # so we don't have to do it explicitly here. 

565 return json.loads(text_(bstruct, encoding='utf-8')) 

566 

567class Base64Serializer(object): 

568 """ A serializer which uses base64 to encode/decode data""" 

569 

570 def __init__(self, serializer=None): 

571 if serializer is None: 

572 serializer = JSONSerializer() 

573 

574 self.serializer = serializer 

575 

576 def dumps(self, appstruct): 

577 """ 

578 Given an ``appstruct``, serialize and sign the data. 

579 

580 Returns a bytestring. 

581 """ 

582 cstruct = self.serializer.dumps(appstruct) # will be bytes 

583 return base64.urlsafe_b64encode(cstruct) 

584 

585 def loads(self, bstruct): 

586 """ 

587 Given a ``bstruct`` (a bytestring), verify the signature and then 

588 deserialize and return the deserialized value. 

589 

590 A ``ValueError`` will be raised if the signature fails to validate. 

591 """ 

592 try: 

593 cstruct = base64.urlsafe_b64decode(bytes_(bstruct)) 

594 except (binascii.Error, TypeError) as e: 

595 raise ValueError('Badly formed base64 data: %s' % e) 

596 

597 return self.serializer.loads(cstruct) 

598 

599class SignedSerializer(object): 

600 """ 

601 A helper to cryptographically sign arbitrary content using HMAC. 

602 

603 The serializer accepts arbitrary functions for performing the actual 

604 serialization and deserialization. 

605 

606 ``secret`` 

607 A string which is used to sign the cookie. The secret should be at 

608 least as long as the block size of the selected hash algorithm. For 

609 ``sha512`` this would mean a 512 bit (64 character) secret. 

610 

611 ``salt`` 

612 A namespace to avoid collisions between different uses of a shared 

613 secret. 

614 

615 ``hashalg`` 

616 The HMAC digest algorithm to use for signing. The algorithm must be 

617 supported by the :mod:`hashlib` library. Default: ``'sha512'``. 

618 

619 ``serializer`` 

620 An object with two methods: `loads`` and ``dumps``. The ``loads`` method 

621 should accept bytes and return a Python object. The ``dumps`` method 

622 should accept a Python object and return bytes. A ``ValueError`` should 

623 be raised for malformed inputs. Default: ``None`, which will use a 

624 derivation of :func:`json.dumps` and ``json.loads``. 

625 

626 """ 

627 

628 def __init__(self, 

629 secret, 

630 salt, 

631 hashalg='sha512', 

632 serializer=None, 

633 ): 

634 self.salt = salt 

635 self.secret = secret 

636 self.hashalg = hashalg 

637 

638 try: 

639 # bwcompat with webob <= 1.3.1, leave latin-1 as the default 

640 self.salted_secret = bytes_(salt or '') + bytes_(secret) 

641 except UnicodeEncodeError: 

642 self.salted_secret = ( 

643 bytes_(salt or '', 'utf-8') + bytes_(secret, 'utf-8')) 

644 

645 self.digestmod = lambda string=b'': hashlib.new(self.hashalg, string) 

646 self.digest_size = self.digestmod().digest_size 

647 

648 if serializer is None: 

649 serializer = JSONSerializer() 

650 

651 self.serializer = serializer 

652 

653 def dumps(self, appstruct): 

654 """ 

655 Given an ``appstruct``, serialize and sign the data. 

656 

657 Returns a bytestring. 

658 """ 

659 cstruct = self.serializer.dumps(appstruct) # will be bytes 

660 sig = hmac.new(self.salted_secret, cstruct, self.digestmod).digest() 

661 return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') 

662 

663 def loads(self, bstruct): 

664 """ 

665 Given a ``bstruct`` (a bytestring), verify the signature and then 

666 deserialize and return the deserialized value. 

667 

668 A ``ValueError`` will be raised if the signature fails to validate. 

669 """ 

670 try: 

671 b64padding = b'=' * (-len(bstruct) % 4) 

672 fstruct = base64.urlsafe_b64decode(bytes_(bstruct) + b64padding) 

673 except (binascii.Error, TypeError) as e: 

674 raise ValueError('Badly formed base64 data: %s' % e) 

675 

676 cstruct = fstruct[self.digest_size:] 

677 expected_sig = fstruct[:self.digest_size] 

678 

679 sig = hmac.new( 

680 self.salted_secret, bytes_(cstruct), self.digestmod).digest() 

681 

682 if strings_differ(sig, expected_sig): 

683 raise ValueError('Invalid signature') 

684 

685 return self.serializer.loads(cstruct) 

686 

687 

688_default = object() 

689 

690class CookieProfile(object): 

691 """ 

692 A helper class that helps bring some sanity to the insanity that is cookie 

693 handling. 

694 

695 The helper is capable of generating multiple cookies if necessary to 

696 support subdomains and parent domains. 

697 

698 ``cookie_name`` 

699 The name of the cookie used for sessioning. Default: ``'session'``. 

700 

701 ``max_age`` 

702 The maximum age of the cookie used for sessioning (in seconds). 

703 Default: ``None`` (browser scope). 

704 

705 ``secure`` 

706 The 'secure' flag of the session cookie. Default: ``False``. 

707 

708 ``httponly`` 

709 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the 

710 session cookie. Default: ``False``. 

711 

712 ``samesite`` 

713 The 'SameSite' attribute of the cookie, can be either ``b"strict"``, 

714 ``b"lax"``, ``b"none"``, or ``None``. 

715 

716 For more information please see the ``samesite`` documentation in 

717 :meth:`webob.cookies.make_cookie` 

718 

719 ``path`` 

720 The path used for the session cookie. Default: ``'/'``. 

721 

722 ``domains`` 

723 The domain(s) used for the session cookie. Default: ``None`` (no domain). 

724 Can be passed an iterable containing multiple domains, this will set 

725 multiple cookies one for each domain. 

726 

727 ``serializer`` 

728 An object with two methods: ``loads`` and ``dumps``. The ``loads`` method 

729 should accept a bytestring and return a Python object. The ``dumps`` 

730 method should accept a Python object and return bytes. A ``ValueError`` 

731 should be raised for malformed inputs. Default: ``None``, which will use 

732 a derivation of :func:`json.dumps` and :func:`json.loads`. 

733 

734 """ 

735 

736 def __init__(self, 

737 cookie_name, 

738 secure=False, 

739 max_age=None, 

740 httponly=None, 

741 samesite=None, 

742 path='/', 

743 domains=None, 

744 serializer=None 

745 ): 

746 self.cookie_name = cookie_name 

747 self.secure = secure 

748 self.max_age = max_age 

749 self.httponly = httponly 

750 self.samesite = samesite 

751 self.path = path 

752 self.domains = domains 

753 

754 if serializer is None: 

755 serializer = Base64Serializer() 

756 

757 self.serializer = serializer 

758 self.request = None 

759 

760 def __call__(self, request): 

761 """ Bind a request to a copy of this instance and return it""" 

762 

763 return self.bind(request) 

764 

765 def bind(self, request): 

766 """ Bind a request to a copy of this instance and return it""" 

767 

768 selfish = CookieProfile( 

769 self.cookie_name, 

770 self.secure, 

771 self.max_age, 

772 self.httponly, 

773 self.samesite, 

774 self.path, 

775 self.domains, 

776 self.serializer, 

777 ) 

778 selfish.request = request 

779 return selfish 

780 

781 def get_value(self): 

782 """ Looks for a cookie by name in the currently bound request, and 

783 returns its value. If the cookie profile is not bound to a request, 

784 this method will raise a :exc:`ValueError`. 

785 

786 Looks for the cookie in the cookies jar, and if it can find it it will 

787 attempt to deserialize it. Returns ``None`` if there is no cookie or 

788 if the value in the cookie cannot be successfully deserialized. 

789 """ 

790 

791 if not self.request: 

792 raise ValueError('No request bound to cookie profile') 

793 

794 cookie = self.request.cookies.get(self.cookie_name) 

795 

796 if cookie is not None: 

797 try: 

798 return self.serializer.loads(bytes_(cookie)) 

799 except ValueError: 

800 return None 

801 

802 def set_cookies(self, response, value, domains=_default, max_age=_default, 

803 path=_default, secure=_default, httponly=_default, 

804 samesite=_default): 

805 """ Set the cookies on a response.""" 

806 cookies = self.get_headers( 

807 value, 

808 domains=domains, 

809 max_age=max_age, 

810 path=path, 

811 secure=secure, 

812 httponly=httponly, 

813 samesite=samesite, 

814 ) 

815 response.headerlist.extend(cookies) 

816 return response 

817 

818 def get_headers(self, value, domains=_default, max_age=_default, 

819 path=_default, secure=_default, httponly=_default, 

820 samesite=_default): 

821 """ Retrieve raw headers for setting cookies. 

822 

823 Returns a list of headers that should be set for the cookies to 

824 be correctly tracked. 

825 """ 

826 if value is None: 

827 max_age = 0 

828 bstruct = None 

829 else: 

830 bstruct = self.serializer.dumps(value) 

831 

832 return self._get_cookies( 

833 bstruct, 

834 domains=domains, 

835 max_age=max_age, 

836 path=path, 

837 secure=secure, 

838 httponly=httponly, 

839 samesite=samesite, 

840 ) 

841 

842 def _get_cookies(self, value, domains, max_age, path, secure, httponly, 

843 samesite): 

844 """Internal function 

845 

846 This returns a list of cookies that are valid HTTP Headers. 

847 

848 :environ: The request environment 

849 :value: The value to store in the cookie 

850 :domains: The domains, overrides any set in the CookieProfile 

851 :max_age: The max_age, overrides any set in the CookieProfile 

852 :path: The path, overrides any set in the CookieProfile 

853 :secure: Set this cookie to secure, overrides any set in CookieProfile 

854 :httponly: Set this cookie to HttpOnly, overrides any set in CookieProfile 

855 :samesite: Set this cookie to be for only the same site, overrides any 

856 set in CookieProfile. 

857 

858 """ 

859 

860 # If the user doesn't provide values, grab the defaults 

861 if domains is _default: 

862 domains = self.domains 

863 

864 if max_age is _default: 

865 max_age = self.max_age 

866 

867 if path is _default: 

868 path = self.path 

869 

870 if secure is _default: 

871 secure = self.secure 

872 

873 if httponly is _default: 

874 httponly = self.httponly 

875 

876 if samesite is _default: 

877 samesite = self.samesite 

878 

879 # Length selected based upon http://browsercookielimits.x64.me 

880 if value is not None and len(value) > 4093: 

881 raise ValueError( 

882 'Cookie value is too long to store (%s bytes)' % 

883 len(value) 

884 ) 

885 

886 cookies = [] 

887 

888 if not domains: 

889 cookievalue = make_cookie( 

890 self.cookie_name, 

891 value, 

892 path=path, 

893 max_age=max_age, 

894 httponly=httponly, 

895 samesite=samesite, 

896 secure=secure 

897 ) 

898 cookies.append(('Set-Cookie', cookievalue)) 

899 

900 else: 

901 for domain in domains: 

902 cookievalue = make_cookie( 

903 self.cookie_name, 

904 value, 

905 path=path, 

906 domain=domain, 

907 max_age=max_age, 

908 httponly=httponly, 

909 samesite=samesite, 

910 secure=secure, 

911 ) 

912 cookies.append(('Set-Cookie', cookievalue)) 

913 

914 return cookies 

915 

916 

917class SignedCookieProfile(CookieProfile): 

918 """ 

919 A helper for generating cookies that are signed to prevent tampering. 

920 

921 By default this will create a single cookie, given a value it will 

922 serialize it, then use HMAC to cryptographically sign the data. Finally 

923 the result is base64-encoded for transport. This way a remote user can 

924 not tamper with the value without uncovering the secret/salt used. 

925 

926 ``secret`` 

927 A string which is used to sign the cookie. The secret should be at 

928 least as long as the block size of the selected hash algorithm. For 

929 ``sha512`` this would mean a 512 bit (64 character) secret. 

930 

931 ``salt`` 

932 A namespace to avoid collisions between different uses of a shared 

933 secret. 

934 

935 ``hashalg`` 

936 The HMAC digest algorithm to use for signing. The algorithm must be 

937 supported by the :mod:`hashlib` library. Default: ``'sha512'``. 

938 

939 ``cookie_name`` 

940 The name of the cookie used for sessioning. Default: ``'session'``. 

941 

942 ``max_age`` 

943 The maximum age of the cookie used for sessioning (in seconds). 

944 Default: ``None`` (browser scope). 

945 

946 ``secure`` 

947 The 'secure' flag of the session cookie. Default: ``False``. 

948 

949 ``httponly`` 

950 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the 

951 session cookie. Default: ``False``. 

952 

953 ``samesite`` 

954 The 'SameSite' attribute of the cookie, can be either ``b"strict"``, 

955 ``b"lax"``, ``b"none"``, or ``None``. 

956 

957 ``path`` 

958 The path used for the session cookie. Default: ``'/'``. 

959 

960 ``domains`` 

961 The domain(s) used for the session cookie. Default: ``None`` (no domain). 

962 Can be passed an iterable containing multiple domains, this will set 

963 multiple cookies one for each domain. 

964 

965 ``serializer`` 

966 An object with two methods: `loads`` and ``dumps``. The ``loads`` method 

967 should accept bytes and return a Python object. The ``dumps`` method 

968 should accept a Python object and return bytes. A ``ValueError`` should 

969 be raised for malformed inputs. Default: ``None`, which will use a 

970 derivation of :func:`json.dumps` and ``json.loads``. 

971 """ 

972 def __init__(self, 

973 secret, 

974 salt, 

975 cookie_name, 

976 secure=False, 

977 max_age=None, 

978 httponly=False, 

979 samesite=None, 

980 path="/", 

981 domains=None, 

982 hashalg='sha512', 

983 serializer=None, 

984 ): 

985 self.secret = secret 

986 self.salt = salt 

987 self.hashalg = hashalg 

988 self.original_serializer = serializer 

989 

990 signed_serializer = SignedSerializer( 

991 secret, 

992 salt, 

993 hashalg, 

994 serializer=self.original_serializer, 

995 ) 

996 CookieProfile.__init__( 

997 self, 

998 cookie_name, 

999 secure=secure, 

1000 max_age=max_age, 

1001 httponly=httponly, 

1002 samesite=samesite, 

1003 path=path, 

1004 domains=domains, 

1005 serializer=signed_serializer, 

1006 ) 

1007 

1008 def bind(self, request): 

1009 """ Bind a request to a copy of this instance and return it""" 

1010 

1011 selfish = SignedCookieProfile( 

1012 self.secret, 

1013 self.salt, 

1014 self.cookie_name, 

1015 self.secure, 

1016 self.max_age, 

1017 self.httponly, 

1018 self.samesite, 

1019 self.path, 

1020 self.domains, 

1021 self.hashalg, 

1022 self.original_serializer, 

1023 ) 

1024 selfish.request = request 

1025 return selfish 

1026