master
py 441 lines 16 KB
Raw
1 # SPDX-License-Identifier: MIT
2 from __future__ import absolute_import
3 import collections
4 import functools
5 import logging
6
7 from ._collections import RecentlyUsedContainer
8 from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
9 from .connectionpool import port_by_scheme
10 from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown
11 from .packages.six.moves.urllib.parse import urljoin
12 from .request import RequestMethods
13 from .util.url import parse_url
14 from .util.retry import Retry
15
16
17 __all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url']
18
19
20 log = logging.getLogger(__name__)
21
22 SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
23 'ssl_version', 'ca_cert_dir', 'ssl_context')
24
25 # All known keyword arguments that could be provided to the pool manager, its
26 # pools, or the underlying connections. This is used to construct a pool key.
27 _key_fields = (
28 'key_scheme', # str
29 'key_host', # str
30 'key_port', # int
31 'key_timeout', # int or float or Timeout
32 'key_retries', # int or Retry
33 'key_strict', # bool
34 'key_block', # bool
35 'key_source_address', # str
36 'key_key_file', # str
37 'key_cert_file', # str
38 'key_cert_reqs', # str
39 'key_ca_certs', # str
40 'key_ssl_version', # str
41 'key_ca_cert_dir', # str
42 'key_ssl_context', # instance of ssl.SSLContext or urllib3.util.ssl_.SSLContext
43 'key_maxsize', # int
44 'key_headers', # dict
45 'key__proxy', # parsed proxy url
46 'key__proxy_headers', # dict
47 'key_socket_options', # list of (level (int), optname (int), value (int or str)) tuples
48 'key__socks_options', # dict
49 'key_assert_hostname', # bool or string
50 'key_assert_fingerprint', # str
51 )
52
53 #: The namedtuple class used to construct keys for the connection pool.
54 #: All custom key schemes should include the fields in this key at a minimum.
55 PoolKey = collections.namedtuple('PoolKey', _key_fields)
56
57
58 def _default_key_normalizer(key_class, request_context):
59 """
60 Create a pool key out of a request context dictionary.
61
62 According to RFC 3986, both the scheme and host are case-insensitive.
63 Therefore, this function normalizes both before constructing the pool
64 key for an HTTPS request. If you wish to change this behaviour, provide
65 alternate callables to ``key_fn_by_scheme``.
66
67 :param key_class:
68 The class to use when constructing the key. This should be a namedtuple
69 with the ``scheme`` and ``host`` keys at a minimum.
70 :type key_class: namedtuple
71 :param request_context:
72 A dictionary-like object that contain the context for a request.
73 :type request_context: dict
74
75 :return: A namedtuple that can be used as a connection pool key.
76 :rtype: PoolKey
77 """
78 # Since we mutate the dictionary, make a copy first
79 context = request_context.copy()
80 context['scheme'] = context['scheme'].lower()
81 context['host'] = context['host'].lower()
82
83 # These are both dictionaries and need to be transformed into frozensets
84 for key in ('headers', '_proxy_headers', '_socks_options'):
85 if key in context and context[key] is not None:
86 context[key] = frozenset(context[key].items())
87
88 # The socket_options key may be a list and needs to be transformed into a
89 # tuple.
90 socket_opts = context.get('socket_options')
91 if socket_opts is not None:
92 context['socket_options'] = tuple(socket_opts)
93
94 # Map the kwargs to the names in the namedtuple - this is necessary since
95 # namedtuples can't have fields starting with '_'.
96 for key in list(context.keys()):
97 context['key_' + key] = context.pop(key)
98
99 # Default to ``None`` for keys missing from the context
100 for field in key_class._fields:
101 if field not in context:
102 context[field] = None
103
104 return key_class(**context)
105
106
107 #: A dictionary that maps a scheme to a callable that creates a pool key.
108 #: This can be used to alter the way pool keys are constructed, if desired.
109 #: Each PoolManager makes a copy of this dictionary so they can be configured
110 #: globally here, or individually on the instance.
111 key_fn_by_scheme = {
112 'http': functools.partial(_default_key_normalizer, PoolKey),
113 'https': functools.partial(_default_key_normalizer, PoolKey),
114 }
115
116 pool_classes_by_scheme = {
117 'http': HTTPConnectionPool,
118 'https': HTTPSConnectionPool,
119 }
120
121
122 class PoolManager(RequestMethods):
123 """
124 Allows for arbitrary requests while transparently keeping track of
125 necessary connection pools for you.
126
127 :param num_pools:
128 Number of connection pools to cache before discarding the least
129 recently used pool.
130
131 :param headers:
132 Headers to include with all requests, unless other headers are given
133 explicitly.
134
135 :param \\**connection_pool_kw:
136 Additional parameters are used to create fresh
137 :class:`urllib3.connectionpool.ConnectionPool` instances.
138
139 Example::
140
141 >>> manager = PoolManager(num_pools=2)
142 >>> r = manager.request('GET', 'http://google.com/')
143 >>> r = manager.request('GET', 'http://google.com/mail')
144 >>> r = manager.request('GET', 'http://yahoo.com/')
145 >>> len(manager.pools)
146 2
147
148 """
149
150 proxy = None
151
152 def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
153 RequestMethods.__init__(self, headers)
154 self.connection_pool_kw = connection_pool_kw
155 self.pools = RecentlyUsedContainer(num_pools,
156 dispose_func=lambda p: p.close())
157
158 # Locally set the pool classes and keys so other PoolManagers can
159 # override them.
160 self.pool_classes_by_scheme = pool_classes_by_scheme
161 self.key_fn_by_scheme = key_fn_by_scheme.copy()
162
163 def __enter__(self):
164 return self
165
166 def __exit__(self, exc_type, exc_val, exc_tb):
167 self.clear()
168 # Return False to re-raise any potential exceptions
169 return False
170
171 def _new_pool(self, scheme, host, port, request_context=None):
172 """
173 Create a new :class:`ConnectionPool` based on host, port, scheme, and
174 any additional pool keyword arguments.
175
176 If ``request_context`` is provided, it is provided as keyword arguments
177 to the pool class used. This method is used to actually create the
178 connection pools handed out by :meth:`connection_from_url` and
179 companion methods. It is intended to be overridden for customization.
180 """
181 pool_cls = self.pool_classes_by_scheme[scheme]
182 if request_context is None:
183 request_context = self.connection_pool_kw.copy()
184
185 # Although the context has everything necessary to create the pool,
186 # this function has historically only used the scheme, host, and port
187 # in the positional args. When an API change is acceptable these can
188 # be removed.
189 for key in ('scheme', 'host', 'port'):
190 request_context.pop(key, None)
191
192 if scheme == 'http':
193 for kw in SSL_KEYWORDS:
194 request_context.pop(kw, None)
195
196 return pool_cls(host, port, **request_context)
197
198 def clear(self):
199 """
200 Empty our store of pools and direct them all to close.
201
202 This will not affect in-flight connections, but they will not be
203 re-used after completion.
204 """
205 self.pools.clear()
206
207 def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None):
208 """
209 Get a :class:`ConnectionPool` based on the host, port, and scheme.
210
211 If ``port`` isn't given, it will be derived from the ``scheme`` using
212 ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is
213 provided, it is merged with the instance's ``connection_pool_kw``
214 variable and used to create the new connection pool, if one is
215 needed.
216 """
217
218 if not host:
219 raise LocationValueError("No host specified.")
220
221 request_context = self._merge_pool_kwargs(pool_kwargs)
222 request_context['scheme'] = scheme or 'http'
223 if not port:
224 port = port_by_scheme.get(request_context['scheme'].lower(), 80)
225 request_context['port'] = port
226 request_context['host'] = host
227
228 return self.connection_from_context(request_context)
229
230 def connection_from_context(self, request_context):
231 """
232 Get a :class:`ConnectionPool` based on the request context.
233
234 ``request_context`` must at least contain the ``scheme`` key and its
235 value must be a key in ``key_fn_by_scheme`` instance variable.
236 """
237 scheme = request_context['scheme'].lower()
238 pool_key_constructor = self.key_fn_by_scheme[scheme]
239 pool_key = pool_key_constructor(request_context)
240
241 return self.connection_from_pool_key(pool_key, request_context=request_context)
242
243 def connection_from_pool_key(self, pool_key, request_context=None):
244 """
245 Get a :class:`ConnectionPool` based on the provided pool key.
246
247 ``pool_key`` should be a namedtuple that only contains immutable
248 objects. At a minimum it must have the ``scheme``, ``host``, and
249 ``port`` fields.
250 """
251 with self.pools.lock:
252 # If the scheme, host, or port doesn't match existing open
253 # connections, open a new ConnectionPool.
254 pool = self.pools.get(pool_key)
255 if pool:
256 return pool
257
258 # Make a fresh ConnectionPool of the desired type
259 scheme = request_context['scheme']
260 host = request_context['host']
261 port = request_context['port']
262 pool = self._new_pool(scheme, host, port, request_context=request_context)
263 self.pools[pool_key] = pool
264
265 return pool
266
267 def connection_from_url(self, url, pool_kwargs=None):
268 """
269 Similar to :func:`urllib3.connectionpool.connection_from_url`.
270
271 If ``pool_kwargs`` is not provided and a new pool needs to be
272 constructed, ``self.connection_pool_kw`` is used to initialize
273 the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs``
274 is provided, it is used instead. Note that if a new pool does not
275 need to be created for the request, the provided ``pool_kwargs`` are
276 not used.
277 """
278 u = parse_url(url)
279 return self.connection_from_host(u.host, port=u.port, scheme=u.scheme,
280 pool_kwargs=pool_kwargs)
281
282 def _merge_pool_kwargs(self, override):
283 """
284 Merge a dictionary of override values for self.connection_pool_kw.
285
286 This does not modify self.connection_pool_kw and returns a new dict.
287 Any keys in the override dictionary with a value of ``None`` are
288 removed from the merged dictionary.
289 """
290 base_pool_kwargs = self.connection_pool_kw.copy()
291 if override:
292 for key, value in override.items():
293 if value is None:
294 try:
295 del base_pool_kwargs[key]
296 except KeyError:
297 pass
298 else:
299 base_pool_kwargs[key] = value
300 return base_pool_kwargs
301
302 def urlopen(self, method, url, redirect=True, **kw):
303 """
304 Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen`
305 with custom cross-host redirect logic and only sends the request-uri
306 portion of the ``url``.
307
308 The given ``url`` parameter must be absolute, such that an appropriate
309 :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
310 """
311 u = parse_url(url)
312 conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
313
314 kw['assert_same_host'] = False
315 kw['redirect'] = False
316 if 'headers' not in kw:
317 kw['headers'] = self.headers
318
319 if self.proxy is not None and u.scheme == "http":
320 response = conn.urlopen(method, url, **kw)
321 else:
322 response = conn.urlopen(method, u.request_uri, **kw)
323
324 redirect_location = redirect and response.get_redirect_location()
325 if not redirect_location:
326 return response
327
328 # Support relative URLs for redirecting.
329 redirect_location = urljoin(url, redirect_location)
330
331 # RFC 7231, Section 6.4.4
332 if response.status == 303:
333 method = 'GET'
334
335 retries = kw.get('retries')
336 if not isinstance(retries, Retry):
337 retries = Retry.from_int(retries, redirect=redirect)
338
339 try:
340 retries = retries.increment(method, url, response=response, _pool=conn)
341 except MaxRetryError:
342 if retries.raise_on_redirect:
343 raise
344 return response
345
346 kw['retries'] = retries
347 kw['redirect'] = redirect
348
349 log.info("Redirecting %s -> %s", url, redirect_location)
350 return self.urlopen(method, redirect_location, **kw)
351
352
353 class ProxyManager(PoolManager):
354 """
355 Behaves just like :class:`PoolManager`, but sends all requests through
356 the defined proxy, using the CONNECT method for HTTPS URLs.
357
358 :param proxy_url:
359 The URL of the proxy to be used.
360
361 :param proxy_headers:
362 A dictionary contaning headers that will be sent to the proxy. In case
363 of HTTP they are being sent with each request, while in the
364 HTTPS/CONNECT case they are sent only once. Could be used for proxy
365 authentication.
366
367 Example:
368 >>> proxy = urllib3.ProxyManager('http://localhost:3128/')
369 >>> r1 = proxy.request('GET', 'http://google.com/')
370 >>> r2 = proxy.request('GET', 'http://httpbin.org/')
371 >>> len(proxy.pools)
372 1
373 >>> r3 = proxy.request('GET', 'https://httpbin.org/')
374 >>> r4 = proxy.request('GET', 'https://twitter.com/')
375 >>> len(proxy.pools)
376 3
377
378 """
379
380 def __init__(self, proxy_url, num_pools=10, headers=None,
381 proxy_headers=None, **connection_pool_kw):
382
383 if isinstance(proxy_url, HTTPConnectionPool):
384 proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host,
385 proxy_url.port)
386 proxy = parse_url(proxy_url)
387 if not proxy.port:
388 port = port_by_scheme.get(proxy.scheme, 80)
389 proxy = proxy._replace(port=port)
390
391 if proxy.scheme not in ("http", "https"):
392 raise ProxySchemeUnknown(proxy.scheme)
393
394 self.proxy = proxy
395 self.proxy_headers = proxy_headers or {}
396
397 connection_pool_kw['_proxy'] = self.proxy
398 connection_pool_kw['_proxy_headers'] = self.proxy_headers
399
400 super(ProxyManager, self).__init__(
401 num_pools, headers, **connection_pool_kw)
402
403 def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None):
404 if scheme == "https":
405 return super(ProxyManager, self).connection_from_host(
406 host, port, scheme, pool_kwargs=pool_kwargs)
407
408 return super(ProxyManager, self).connection_from_host(
409 self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs)
410
411 def _set_proxy_headers(self, url, headers=None):
412 """
413 Sets headers needed by proxies: specifically, the Accept and Host
414 headers. Only sets headers not provided by the user.
415 """
416 headers_ = {'Accept': '*/*'}
417
418 netloc = parse_url(url).netloc
419 if netloc:
420 headers_['Host'] = netloc
421
422 if headers:
423 headers_.update(headers)
424 return headers_
425
426 def urlopen(self, method, url, redirect=True, **kw):
427 "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
428 u = parse_url(url)
429
430 if u.scheme == "http":
431 # For proxied HTTPS requests, httplib sets the necessary headers
432 # on the CONNECT to the proxy. For HTTP, we'll definitely
433 # need to set 'Host' at the very least.
434 headers = kw.get('headers', self.headers)
435 kw['headers'] = self._set_proxy_headers(url, headers)
436
437 return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw)
438
439
440 def proxy_from_url(url, **kw):
441 return ProxyManager(proxy_url=url, **kw)