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