1"""
2requests.cookies
3~~~~~~~~~~~~~~~~
4
5Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.
6
7requests.utils imports from here, so be careful with imports.
8"""
9
10from __future__ import annotations
11
12import calendar
13import copy
14import time
15from collections.abc import Iterator, MutableMapping
16from http.cookiejar import Cookie, CookieJar, CookiePolicy
17from typing import TYPE_CHECKING, Any, TypeVar, overload
18
19from ._internal_utils import to_native_string
20from ._types import is_prepared as _is_prepared
21from .compat import Morsel, cookielib, urlparse, urlunparse
22
23if TYPE_CHECKING:
24 from _typeshed import SupportsKeysAndGetItem
25
26 from .models import PreparedRequest
27
28import threading
29
30
31class MockRequest:
32 """Wraps a `requests.PreparedRequest` to mimic a `urllib2.Request`.
33
34 The code in `http.cookiejar.CookieJar` expects this interface in order to correctly
35 manage cookie policies, i.e., determine whether a cookie can be set, given the
36 domains of the request and the cookie.
37
38 The original request object is read-only. The client is responsible for collecting
39 the new headers via `get_new_headers()` and interpreting them appropriately. You
40 probably want `get_cookie_header`, defined below.
41 """
42
43 type: str
44
45 def __init__(self, request: PreparedRequest) -> None:
46 assert _is_prepared(request)
47 self._r = request
48 self._new_headers: dict[str, str] = {}
49 self.type = urlparse(self._r.url).scheme
50
51 def get_type(self) -> str:
52 return self.type
53
54 def get_host(self) -> str:
55 return urlparse(self._r.url).netloc
56
57 def get_origin_req_host(self) -> str:
58 return self.get_host()
59
60 def get_full_url(self) -> str:
61 # Only return the response's URL if the user hadn't set the Host
62 # header
63 if not self._r.headers.get("Host"):
64 return self._r.url
65 # If they did set it, retrieve it and reconstruct the expected domain
66 host = to_native_string(self._r.headers["Host"], encoding="utf-8")
67 parsed = urlparse(self._r.url)
68 # Reconstruct the URL as we expect it
69 return urlunparse(
70 [
71 parsed.scheme,
72 host,
73 parsed.path,
74 parsed.params,
75 parsed.query,
76 parsed.fragment,
77 ]
78 )
79
80 def is_unverifiable(self) -> bool:
81 return True
82
83 def has_header(self, name: str) -> bool:
84 return name in self._r.headers or name in self._new_headers
85
86 def get_header(self, name: str, default: str | None = None) -> str | None:
87 return self._r.headers.get(name, self._new_headers.get(name, default)) # type: ignore[return-value]
88
89 def add_header(self, key: str, val: str) -> None:
90 """cookiejar has no legitimate use for this method; add it back if you find one."""
91 raise NotImplementedError(
92 "Cookie headers should be added with add_unredirected_header()"
93 )
94
95 def add_unredirected_header(self, name: str, value: str) -> None:
96 self._new_headers[name] = value
97
98 def get_new_headers(self) -> dict[str, str]:
99 return self._new_headers
100
101 @property
102 def unverifiable(self) -> bool:
103 return self.is_unverifiable()
104
105 @property
106 def origin_req_host(self) -> str:
107 return self.get_origin_req_host()
108
109 @property
110 def host(self) -> str:
111 return self.get_host()
112
113
114class MockResponse:
115 """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
116
117 ...what? Basically, expose the parsed HTTP headers from the server response
118 the way `http.cookiejar` expects to see them.
119 """
120
121 def __init__(self, headers: Any) -> None:
122 """Make a MockResponse for `cookiejar` to read.
123
124 :param headers: a httplib.HTTPMessage or analogous carrying the headers
125 """
126 self._headers = headers
127
128 def info(self) -> Any:
129 return self._headers
130
131 def getheaders(self, name: str) -> Any:
132 self._headers.getheaders(name)
133
134
135def extract_cookies_to_jar(
136 jar: CookieJar, request: PreparedRequest, response: Any
137) -> None:
138 """Extract the cookies from the response into a CookieJar.
139
140 :param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar)
141 :param request: our own requests.Request object
142 :param response: urllib3.HTTPResponse object
143 """
144 if not (hasattr(response, "_original_response") and response._original_response):
145 return
146 # the _original_response field is the wrapped httplib.HTTPResponse object,
147 req = MockRequest(request)
148 # pull out the HTTPMessage with the headers and put it in the mock:
149 res = MockResponse(response._original_response.msg)
150 jar.extract_cookies(res, req) # type: ignore[arg-type]
151
152
153def get_cookie_header(jar: CookieJar, request: PreparedRequest) -> str | None:
154 """
155 Produce an appropriate Cookie header string to be sent with `request`, or None.
156
157 :rtype: str
158 """
159 r = MockRequest(request)
160 jar.add_cookie_header(r) # type: ignore[arg-type]
161 return r.get_new_headers().get("Cookie")
162
163
164def remove_cookie_by_name(
165 cookiejar: CookieJar, name: str, domain: str | None = None, path: str | None = None
166) -> None:
167 """Unsets a cookie by name, by default over all domains and paths.
168
169 Wraps CookieJar.clear(), is O(n).
170 """
171 clearables: list[tuple[str, str, str]] = []
172 for cookie in cookiejar:
173 if cookie.name != name:
174 continue
175 if domain is not None and domain != cookie.domain:
176 continue
177 if path is not None and path != cookie.path:
178 continue
179 clearables.append((cookie.domain, cookie.path, cookie.name))
180
181 for domain, path, name in clearables:
182 cookiejar.clear(domain, path, name)
183
184
185class CookieConflictError(RuntimeError):
186 """There are two cookies that meet the criteria specified in the cookie jar.
187 Use .get and .set and include domain and path args in order to be more specific.
188 """
189
190
191class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc]
192 """Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict
193 interface.
194
195 This is the CookieJar we create by default for requests and sessions that
196 don't specify one, since some clients may expect response.cookies and
197 session.cookies to support dict operations.
198
199 Requests does not use the dict interface internally; it's just for
200 compatibility with external client code. All requests code should work
201 out of the box with externally provided instances of ``CookieJar``, e.g.
202 ``LWPCookieJar`` and ``FileCookieJar``.
203
204 Unlike a regular CookieJar, this class is pickleable.
205
206 .. warning:: dictionary operations that are normally O(1) may be O(n).
207 """
208
209 _policy: CookiePolicy
210
211 def get( # type: ignore[override]
212 self,
213 name: str,
214 default: str | None = None,
215 domain: str | None = None,
216 path: str | None = None,
217 ) -> str | None:
218 """Dict-like get() that also supports optional domain and path args in
219 order to resolve naming collisions from using one cookie jar over
220 multiple domains.
221
222 .. warning:: operation is O(n), not O(1).
223 """
224 try:
225 return self._find_no_duplicates(name, domain, path)
226 except KeyError:
227 return default
228
229 def set(
230 self, name: str, value: str | Morsel[dict[str, str]] | None, **kwargs: Any
231 ) -> Cookie | None:
232 """Dict-like set() that also supports optional domain and path args in
233 order to resolve naming collisions from using one cookie jar over
234 multiple domains.
235 """
236 # support client code that unsets cookies by assignment of a None value:
237 if value is None:
238 remove_cookie_by_name(
239 self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
240 )
241 return
242
243 if isinstance(value, Morsel):
244 c = morsel_to_cookie(value)
245 else:
246 c = create_cookie(name, value, **kwargs)
247 self.set_cookie(c)
248 return c
249
250 def iterkeys(self) -> Iterator[str]:
251 """Dict-like iterkeys() that returns an iterator of names of cookies
252 from the jar.
253
254 .. seealso:: itervalues() and iteritems().
255 """
256 for cookie in iter(self):
257 yield cookie.name
258
259 def keys(self) -> list[str]: # type: ignore[override]
260 """Dict-like keys() that returns a list of names of cookies from the
261 jar.
262
263 .. seealso:: values() and items().
264 """
265 return list(self.iterkeys())
266
267 def itervalues(self) -> Iterator[str | None]:
268 """Dict-like itervalues() that returns an iterator of values of cookies
269 from the jar.
270
271 .. seealso:: iterkeys() and iteritems().
272 """
273 for cookie in iter(self):
274 yield cookie.value
275
276 def values(self) -> list[str | None]: # type: ignore[override]
277 """Dict-like values() that returns a list of values of cookies from the
278 jar.
279
280 .. seealso:: keys() and items().
281 """
282 return list(self.itervalues())
283
284 def iteritems(self) -> Iterator[tuple[str, str | None]]:
285 """Dict-like iteritems() that returns an iterator of name-value tuples
286 from the jar.
287
288 .. seealso:: iterkeys() and itervalues().
289 """
290 for cookie in iter(self):
291 yield cookie.name, cookie.value
292
293 def items(self) -> list[tuple[str, str | None]]: # type: ignore[override]
294 """Dict-like items() that returns a list of name-value tuples from the
295 jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a
296 vanilla python dict of key value pairs.
297
298 .. seealso:: keys() and values().
299 """
300 return list(self.iteritems())
301
302 def list_domains(self) -> list[str]:
303 """Utility method to list all the domains in the jar."""
304 domains: list[str] = []
305 for cookie in iter(self):
306 if cookie.domain not in domains:
307 domains.append(cookie.domain)
308 return domains
309
310 def list_paths(self) -> list[str]:
311 """Utility method to list all the paths in the jar."""
312 paths: list[str] = []
313 for cookie in iter(self):
314 if cookie.path not in paths:
315 paths.append(cookie.path)
316 return paths
317
318 def multiple_domains(self) -> bool:
319 """Returns True if there are multiple domains in the jar.
320 Returns False otherwise.
321
322 :rtype: bool
323 """
324 domains: list[str] = []
325 for cookie in iter(self):
326 if cookie.domain is not None and cookie.domain in domains: # type: ignore[reportUnnecessaryComparison] # defensive check
327 return True
328 domains.append(cookie.domain)
329 return False # there is only one domain in jar
330
331 def get_dict(
332 self, domain: str | None = None, path: str | None = None
333 ) -> dict[str, str | None]:
334 """Takes as an argument an optional domain and path and returns a plain
335 old Python dict of name-value pairs of cookies that meet the
336 requirements.
337
338 :rtype: dict
339 """
340 dictionary: dict[str, str | None] = {}
341 for cookie in iter(self):
342 if (domain is None or cookie.domain == domain) and (
343 path is None or cookie.path == path
344 ):
345 dictionary[cookie.name] = cookie.value
346 return dictionary
347
348 def __iter__(self) -> Iterator[Cookie]: # type: ignore[override]
349 """RequestCookieJar's __iter__ comes from CookieJar not MutableMapping."""
350 return super().__iter__()
351
352 def __contains__(self, name: object) -> bool:
353 try:
354 return super().__contains__(name)
355 except CookieConflictError:
356 return True
357
358 def __getitem__(self, name: str) -> str | None:
359 """Dict-like __getitem__() for compatibility with client code. Throws
360 exception if there are more than one cookie with name. In that case,
361 use the more explicit get() method instead.
362
363 .. warning:: operation is O(n), not O(1).
364 """
365 return self._find_no_duplicates(name)
366
367 def __setitem__(
368 self, name: str, value: str | Morsel[dict[str, str]] | None
369 ) -> None:
370 """Dict-like __setitem__ for compatibility with client code. Throws
371 exception if there is already a cookie of that name in the jar. In that
372 case, use the more explicit set() method instead.
373 """
374 self.set(name, value)
375
376 def __delitem__(self, name: str) -> None:
377 """Deletes a cookie given a name. Wraps ``http.cookiejar.CookieJar``'s
378 ``remove_cookie_by_name()``.
379 """
380 remove_cookie_by_name(self, name)
381
382 def set_cookie(self, cookie: Cookie, *args: Any, **kwargs: Any) -> None:
383 if (
384 (value := cookie.value) is not None
385 and value.startswith('"')
386 and value.endswith('"')
387 ):
388 cookie.value = value.replace('\\"', "")
389 return super().set_cookie(cookie, *args, **kwargs)
390
391 def update( # type: ignore[override]
392 self, other: CookieJar | SupportsKeysAndGetItem[str, str]
393 ) -> None:
394 """Updates this jar with cookies from another CookieJar or dict-like"""
395 if isinstance(other, cookielib.CookieJar):
396 for cookie in other:
397 self.set_cookie(copy.copy(cookie))
398 else:
399 super().update(other)
400
401 def _find(
402 self, name: str, domain: str | None = None, path: str | None = None
403 ) -> str | None:
404 """Requests uses this method internally to get cookie values.
405
406 If there are conflicting cookies, _find arbitrarily chooses one.
407 See _find_no_duplicates if you want an exception thrown if there are
408 conflicting cookies.
409
410 :param name: a string containing name of cookie
411 :param domain: (optional) string containing domain of cookie
412 :param path: (optional) string containing path of cookie
413 :return: cookie.value
414 """
415 for cookie in iter(self):
416 if cookie.name == name:
417 if domain is None or cookie.domain == domain:
418 if path is None or cookie.path == path:
419 return cookie.value
420
421 raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
422
423 def _find_no_duplicates(
424 self, name: str, domain: str | None = None, path: str | None = None
425 ) -> str:
426 """Both ``__get_item__`` and ``get`` call this function: it's never
427 used elsewhere in Requests.
428
429 :param name: a string containing name of cookie
430 :param domain: (optional) string containing domain of cookie
431 :param path: (optional) string containing path of cookie
432 :raises KeyError: if cookie is not found
433 :raises CookieConflictError: if there are multiple cookies
434 that match name and optionally domain and path
435 :return: cookie.value
436 """
437 toReturn = None
438 for cookie in iter(self):
439 if cookie.name == name:
440 if domain is None or cookie.domain == domain:
441 if path is None or cookie.path == path:
442 if toReturn is not None:
443 # if there are multiple cookies that meet passed in criteria
444 raise CookieConflictError(
445 f"There are multiple cookies with name, {name!r}"
446 )
447 # we will eventually return this as long as no cookie conflict
448 toReturn = cookie.value
449
450 if toReturn is not None:
451 return toReturn
452 raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
453
454 def __getstate__(self) -> dict[str, Any]:
455 """Unlike a normal CookieJar, this class is pickleable."""
456 state = self.__dict__.copy()
457 # remove the unpickleable RLock object
458 state.pop("_cookies_lock")
459 return state
460
461 def __setstate__(self, state: dict[str, Any]) -> None:
462 """Unlike a normal CookieJar, this class is pickleable."""
463 self.__dict__.update(state)
464 if "_cookies_lock" not in self.__dict__:
465 self._cookies_lock = threading.RLock()
466
467 def copy(self) -> RequestsCookieJar:
468 """Return a copy of this RequestsCookieJar."""
469 new_cj = RequestsCookieJar()
470 new_cj.set_policy(self.get_policy())
471 new_cj.update(self)
472 return new_cj
473
474 def get_policy(self) -> CookiePolicy:
475 """Return the CookiePolicy instance used."""
476 return self._policy
477
478
479def _copy_cookie_jar(jar: CookieJar | None) -> CookieJar | None: # type: ignore[reportUnusedFunction] # cross-module usage in models.py
480 if jar is None:
481 return None
482
483 if copy_method := getattr(jar, "copy", None):
484 # We're dealing with an instance of RequestsCookieJar
485 return copy_method()
486 # We're dealing with a generic CookieJar instance
487 new_jar = copy.copy(jar)
488 new_jar.clear()
489 for cookie in jar:
490 new_jar.set_cookie(copy.copy(cookie))
491 return new_jar
492
493
494def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie:
495 """Make a cookie from underspecified parameters.
496
497 By default, the pair of `name` and `value` will be set for the domain ''
498 and sent on every request (this is sometimes called a "supercookie").
499 """
500 result: dict[str, Any] = {
501 "version": 0,
502 "name": name,
503 "value": value,
504 "port": None,
505 "domain": "",
506 "path": "/",
507 "secure": False,
508 "expires": None,
509 "discard": True,
510 "comment": None,
511 "comment_url": None,
512 "rest": {"HttpOnly": None},
513 "rfc2109": False,
514 }
515
516 badargs = set(kwargs) - set(result)
517 if badargs:
518 raise TypeError(
519 f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
520 )
521
522 result.update(kwargs)
523 result["port_specified"] = bool(result["port"])
524 result["domain_specified"] = bool(result["domain"])
525 result["domain_initial_dot"] = result["domain"].startswith(".")
526 result["path_specified"] = bool(result["path"])
527
528 return cookielib.Cookie(**result)
529
530
531def morsel_to_cookie(morsel: Morsel[Any]) -> Cookie:
532 """Convert a Morsel object into a Cookie containing the one k/v pair."""
533
534 expires: int | None = None
535 if morsel["max-age"]:
536 try:
537 expires = int(time.time() + int(morsel["max-age"]))
538 except ValueError:
539 raise TypeError(f"max-age: {morsel['max-age']} must be integer")
540 elif morsel["expires"]:
541 time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
542 expires = calendar.timegm(time.strptime(morsel["expires"], time_template))
543 return create_cookie(
544 comment=morsel["comment"],
545 comment_url=bool(morsel["comment"]),
546 discard=False,
547 domain=morsel["domain"],
548 expires=expires,
549 name=morsel.key,
550 path=morsel["path"],
551 port=None,
552 rest={"HttpOnly": morsel["httponly"]},
553 rfc2109=False,
554 secure=bool(morsel["secure"]),
555 value=morsel.value,
556 version=morsel["version"] or 0,
557 )
558
559
560_CookieJarT = TypeVar("_CookieJarT", bound=CookieJar)
561
562
563@overload
564def cookiejar_from_dict(
565 cookie_dict: dict[str, str] | None,
566 cookiejar: None = None,
567 overwrite: bool = True,
568) -> RequestsCookieJar: ...
569
570
571@overload
572def cookiejar_from_dict(
573 cookie_dict: dict[str, str] | None,
574 cookiejar: _CookieJarT,
575 overwrite: bool = True,
576) -> _CookieJarT: ...
577
578
579def cookiejar_from_dict(
580 cookie_dict: dict[str, str] | None,
581 cookiejar: CookieJar | None = None,
582 overwrite: bool = True,
583) -> CookieJar:
584 """Returns a CookieJar from a key/value dictionary.
585
586 :param cookie_dict: Dict of key/values to insert into CookieJar.
587 :param cookiejar: (optional) A cookiejar to add the cookies to.
588 :param overwrite: (optional) If False, will not replace cookies
589 already in the jar with new ones.
590 :rtype: CookieJar
591 """
592 if cookiejar is None:
593 cookiejar = RequestsCookieJar()
594
595 if cookie_dict is not None:
596 names_from_jar = [cookie.name for cookie in cookiejar]
597 for name in cookie_dict:
598 if overwrite or (name not in names_from_jar):
599 cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
600
601 return cookiejar
602
603
604def merge_cookies(
605 cookiejar: CookieJar, cookies: dict[str, str] | CookieJar | None
606) -> CookieJar:
607 """Add cookies to cookiejar and returns a merged CookieJar.
608
609 :param cookiejar: CookieJar object to add the cookies to.
610 :param cookies: Dictionary or CookieJar object to be added.
611 :rtype: CookieJar
612 """
613 if not isinstance(cookiejar, cookielib.CookieJar): # type: ignore[reportUnnecessaryIsInstance] # runtime guard
614 raise ValueError("You can only merge into CookieJar")
615
616 if isinstance(cookies, dict):
617 cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)
618 elif isinstance(cookies, cookielib.CookieJar):
619 if update_method := getattr(cookiejar, "update", None):
620 update_method(cookies)
621 else:
622 for cookie_in_jar in cookies:
623 cookiejar.set_cookie(cookie_in_jar)
624
625 return cookiejar