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