← requests  /  src/requests/cookies.py

1
"""
2
requests.cookies
3
~~~~~~~~~~~~~~~~
4
5
Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.
6
7
requests.utils imports from here, so be careful with imports.
8
"""
9
10
from __future__ import annotations
11
12
import calendar
13
import copy
14
import time
15
from collections.abc import Iterator, MutableMapping
16
from http.cookiejar import Cookie, CookieJar, CookiePolicy
17
from typing import TYPE_CHECKING, Any, TypeVar, overload
18
19
from ._internal_utils import to_native_string
20
from ._types import is_prepared as _is_prepared
21
from .compat import Morsel, cookielib, urlparse, urlunparse
22
23
if TYPE_CHECKING:
24
    from _typeshed import SupportsKeysAndGetItem
25
26
    from .models import PreparedRequest
27
28
import threading
29
30
31
class 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
114
class 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
135
def 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
153
def 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
164
def 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
185
class 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
191
class 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
479
def _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
494
def 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
531
def 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
564
def cookiejar_from_dict(
565
    cookie_dict: dict[str, str] | None,
566
    cookiejar: None = None,
567
    overwrite: bool = True,
568
) -> RequestsCookieJar: ...
569
570
571
@overload
572
def cookiejar_from_dict(
573
    cookie_dict: dict[str, str] | None,
574
    cookiejar: _CookieJarT,
575
    overwrite: bool = True,
576
) -> _CookieJarT: ...
577
578
579
def 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
604
def 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
626