Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/urllib3/poolmanager.py: 35%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import functools
4import logging
5import typing
6import warnings
7from types import TracebackType
8from urllib.parse import urljoin
10from ._collections import HTTPHeaderDict, RecentlyUsedContainer
11from ._request_methods import RequestMethods
12from .connection import ProxyConfig
13from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme
14from .exceptions import (
15 LocationValueError,
16 MaxRetryError,
17 ProxySchemeUnknown,
18 URLSchemeUnknown,
19)
20from .response import BaseHTTPResponse
21from .util.connection import _TYPE_SOCKET_OPTIONS
22from .util.proxy import connection_requires_http_tunnel
23from .util.retry import Retry
24from .util.timeout import Timeout
25from .util.url import Url, parse_url
27if typing.TYPE_CHECKING:
28 import ssl
30 from typing_extensions import Self
32__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"]
35log = logging.getLogger(__name__)
37SSL_KEYWORDS = (
38 "key_file",
39 "cert_file",
40 "cert_reqs",
41 "ca_certs",
42 "ca_cert_data",
43 "ssl_version",
44 "ssl_minimum_version",
45 "ssl_maximum_version",
46 "ca_cert_dir",
47 "ssl_context",
48 "key_password",
49 "server_hostname",
50 "assert_hostname",
51 "assert_fingerprint",
52)
53# Default value for `blocksize` - a new parameter introduced to
54# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7
55_DEFAULT_BLOCKSIZE = 16384
58class PoolKey(typing.NamedTuple):
59 """
60 All known keyword arguments that could be provided to the pool manager, its
61 pools, or the underlying connections.
63 All custom key schemes should include the fields in this key at a minimum.
64 """
66 key_scheme: str
67 key_host: str
68 key_port: int | None
69 key_timeout: Timeout | float | int | None
70 key_retries: Retry | bool | int | None
71 key_block: bool | None
72 key_source_address: tuple[str, int] | None
73 key_key_file: str | None
74 key_key_password: str | None
75 key_cert_file: str | None
76 key_cert_reqs: str | None
77 key_ca_certs: str | None
78 key_ca_cert_data: str | bytes | None
79 key_ssl_version: int | str | None
80 key_ssl_minimum_version: ssl.TLSVersion | None
81 key_ssl_maximum_version: ssl.TLSVersion | None
82 key_ca_cert_dir: str | None
83 key_ssl_context: ssl.SSLContext | None
84 key_maxsize: int | None
85 key_headers: frozenset[tuple[str, str]] | None
86 key__proxy: Url | None
87 key__proxy_headers: frozenset[tuple[str, str]] | None
88 key__proxy_config: ProxyConfig | None
89 key_socket_options: _TYPE_SOCKET_OPTIONS | None
90 key__socks_options: frozenset[tuple[str, str]] | None
91 key_assert_hostname: bool | str | None
92 key_assert_fingerprint: str | None
93 key_server_hostname: str | None
94 key_blocksize: int | None
97def _default_key_normalizer(
98 key_class: type[PoolKey], request_context: dict[str, typing.Any]
99) -> PoolKey:
100 """
101 Create a pool key out of a request context dictionary.
103 According to RFC 3986, both the scheme and host are case-insensitive.
104 Therefore, this function normalizes both before constructing the pool
105 key for an HTTPS request. If you wish to change this behaviour, provide
106 alternate callables to ``key_fn_by_scheme``.
108 :param key_class:
109 The class to use when constructing the key. This should be a namedtuple
110 with the ``scheme`` and ``host`` keys at a minimum.
111 :type key_class: namedtuple
112 :param request_context:
113 A dictionary-like object that contain the context for a request.
114 :type request_context: dict
116 :return: A namedtuple that can be used as a connection pool key.
117 :rtype: PoolKey
118 """
119 # Since we mutate the dictionary, make a copy first
120 context = request_context.copy()
121 context["scheme"] = context["scheme"].lower()
122 context["host"] = context["host"].lower()
124 # These are both dictionaries and need to be transformed into frozensets
125 for key in ("headers", "_proxy_headers", "_socks_options"):
126 if key in context and context[key] is not None:
127 context[key] = frozenset(context[key].items())
129 # The socket_options key may be a list and needs to be transformed into a
130 # tuple.
131 socket_opts = context.get("socket_options")
132 if socket_opts is not None:
133 context["socket_options"] = tuple(socket_opts)
135 # Map the kwargs to the names in the namedtuple - this is necessary since
136 # namedtuples can't have fields starting with '_'.
137 for key in list(context.keys()):
138 context["key_" + key] = context.pop(key)
140 # Default to ``None`` for keys missing from the context
141 for field in key_class._fields:
142 if field not in context:
143 context[field] = None
145 # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context
146 if context.get("key_blocksize") is None:
147 context["key_blocksize"] = _DEFAULT_BLOCKSIZE
149 return key_class(**context)
152#: A dictionary that maps a scheme to a callable that creates a pool key.
153#: This can be used to alter the way pool keys are constructed, if desired.
154#: Each PoolManager makes a copy of this dictionary so they can be configured
155#: globally here, or individually on the instance.
156key_fn_by_scheme = {
157 "http": functools.partial(_default_key_normalizer, PoolKey),
158 "https": functools.partial(_default_key_normalizer, PoolKey),
159}
161pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool}
164class PoolManager(RequestMethods):
165 """
166 Allows for arbitrary requests while transparently keeping track of
167 necessary connection pools for you.
169 :param num_pools:
170 Number of connection pools to cache before discarding the least
171 recently used pool.
173 :param headers:
174 Headers to include with all requests, unless other headers are given
175 explicitly.
177 :param \\**connection_pool_kw:
178 Additional parameters are used to create fresh
179 :class:`urllib3.connectionpool.ConnectionPool` instances.
181 Example:
183 .. code-block:: python
185 import urllib3
187 http = urllib3.PoolManager(num_pools=2)
189 resp1 = http.request("GET", "https://google.com/")
190 resp2 = http.request("GET", "https://google.com/mail")
191 resp3 = http.request("GET", "https://yahoo.com/")
193 print(len(http.pools))
194 # 2
196 """
198 proxy: Url | None = None
199 proxy_config: ProxyConfig | None = None
201 def __init__(
202 self,
203 num_pools: int = 10,
204 headers: typing.Mapping[str, str] | None = None,
205 **connection_pool_kw: typing.Any,
206 ) -> None:
207 super().__init__(headers)
208 # PoolManager handles redirects itself in PoolManager.urlopen().
209 # It always passes redirect=False to the underlying connection pool to
210 # suppress per-pool redirect handling. If the user supplied a non-Retry
211 # value (int/bool/etc) for retries and we let the pool normalize it
212 # while redirect=False, the resulting Retry object would have redirect
213 # handling disabled, which can interfere with PoolManager's own
214 # redirect logic. Normalize here so redirects remain governed solely by
215 # PoolManager logic.
216 if "retries" in connection_pool_kw:
217 retries = connection_pool_kw["retries"]
218 if not isinstance(retries, Retry):
219 retries = Retry.from_int(retries)
220 connection_pool_kw = connection_pool_kw.copy()
221 connection_pool_kw["retries"] = retries
222 self.connection_pool_kw = connection_pool_kw
224 self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool]
225 self.pools = RecentlyUsedContainer(num_pools)
227 # Locally set the pool classes and keys so other PoolManagers can
228 # override them.
229 self.pool_classes_by_scheme = pool_classes_by_scheme
230 self.key_fn_by_scheme = key_fn_by_scheme.copy()
232 def __enter__(self) -> Self:
233 return self
235 def __exit__(
236 self,
237 exc_type: type[BaseException] | None,
238 exc_val: BaseException | None,
239 exc_tb: TracebackType | None,
240 ) -> typing.Literal[False]:
241 self.clear()
242 # Return False to re-raise any potential exceptions
243 return False
245 def _new_pool(
246 self,
247 scheme: str,
248 host: str,
249 port: int,
250 request_context: dict[str, typing.Any] | None = None,
251 ) -> HTTPConnectionPool:
252 """
253 Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and
254 any additional pool keyword arguments.
256 If ``request_context`` is provided, it is provided as keyword arguments
257 to the pool class used. This method is used to actually create the
258 connection pools handed out by :meth:`connection_from_url` and
259 companion methods. It is intended to be overridden for customization.
260 """
261 pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme]
262 if request_context is None:
263 request_context = self.connection_pool_kw.copy()
265 # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly
266 # set to 'None' in the request_context.
267 if request_context.get("blocksize") is None:
268 request_context["blocksize"] = _DEFAULT_BLOCKSIZE
270 # Although the context has everything necessary to create the pool,
271 # this function has historically only used the scheme, host, and port
272 # in the positional args. When an API change is acceptable these can
273 # be removed.
274 for key in ("scheme", "host", "port"):
275 request_context.pop(key, None)
277 if scheme == "http":
278 for kw in SSL_KEYWORDS:
279 request_context.pop(kw, None)
281 return pool_cls(host, port, **request_context)
283 def clear(self) -> None:
284 """
285 Empty our store of pools and direct them all to close.
287 This will not affect in-flight connections, but they will not be
288 re-used after completion.
289 """
290 self.pools.clear()
292 def connection_from_host(
293 self,
294 host: str | None,
295 port: int | None = None,
296 scheme: str | None = "http",
297 pool_kwargs: dict[str, typing.Any] | None = None,
298 ) -> HTTPConnectionPool:
299 """
300 Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme.
302 If ``port`` isn't given, it will be derived from the ``scheme`` using
303 ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is
304 provided, it is merged with the instance's ``connection_pool_kw``
305 variable and used to create the new connection pool, if one is
306 needed.
307 """
309 if not host:
310 raise LocationValueError("No host specified.")
312 request_context = self._merge_pool_kwargs(pool_kwargs)
313 request_context["scheme"] = scheme or "http"
314 if port is None:
315 port = port_by_scheme.get(request_context["scheme"].lower(), 80)
316 request_context["port"] = port
317 request_context["host"] = host
319 return self.connection_from_context(request_context)
321 def connection_from_context(
322 self, request_context: dict[str, typing.Any]
323 ) -> HTTPConnectionPool:
324 """
325 Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context.
327 ``request_context`` must at least contain the ``scheme`` key and its
328 value must be a key in ``key_fn_by_scheme`` instance variable.
329 """
330 if "strict" in request_context:
331 warnings.warn(
332 "The 'strict' parameter is no longer needed on Python 3+. "
333 "This will raise an error in urllib3 v3.0.",
334 FutureWarning,
335 )
336 request_context.pop("strict")
338 scheme = request_context["scheme"].lower()
339 pool_key_constructor = self.key_fn_by_scheme.get(scheme)
340 if not pool_key_constructor:
341 raise URLSchemeUnknown(scheme)
342 pool_key = pool_key_constructor(request_context)
344 return self.connection_from_pool_key(pool_key, request_context=request_context)
346 def connection_from_pool_key(
347 self, pool_key: PoolKey, request_context: dict[str, typing.Any]
348 ) -> HTTPConnectionPool:
349 """
350 Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key.
352 ``pool_key`` should be a namedtuple that only contains immutable
353 objects. At a minimum it must have the ``scheme``, ``host``, and
354 ``port`` fields.
355 """
356 with self.pools.lock:
357 # If the scheme, host, or port doesn't match existing open
358 # connections, open a new ConnectionPool.
359 pool = self.pools.get(pool_key)
360 if pool:
361 return pool
363 # Make a fresh ConnectionPool of the desired type
364 scheme = request_context["scheme"]
365 host = request_context["host"]
366 port = request_context["port"]
367 pool = self._new_pool(scheme, host, port, request_context=request_context)
368 self.pools[pool_key] = pool
370 return pool
372 def connection_from_url(
373 self, url: str, pool_kwargs: dict[str, typing.Any] | None = None
374 ) -> HTTPConnectionPool:
375 """
376 Similar to :func:`urllib3.connectionpool.connection_from_url`.
378 If ``pool_kwargs`` is not provided and a new pool needs to be
379 constructed, ``self.connection_pool_kw`` is used to initialize
380 the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs``
381 is provided, it is used instead. Note that if a new pool does not
382 need to be created for the request, the provided ``pool_kwargs`` are
383 not used.
384 """
385 u = parse_url(url)
386 return self.connection_from_host(
387 u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs
388 )
390 def _merge_pool_kwargs(
391 self, override: dict[str, typing.Any] | None
392 ) -> dict[str, typing.Any]:
393 """
394 Merge a dictionary of override values for self.connection_pool_kw.
396 This does not modify self.connection_pool_kw and returns a new dict.
397 Any keys in the override dictionary with a value of ``None`` are
398 removed from the merged dictionary.
399 """
400 base_pool_kwargs = self.connection_pool_kw.copy()
401 if override:
402 for key, value in override.items():
403 if value is None:
404 try:
405 del base_pool_kwargs[key]
406 except KeyError:
407 pass
408 else:
409 base_pool_kwargs[key] = value
410 return base_pool_kwargs
412 def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool:
413 """
414 Indicates if the proxy requires the complete destination URL in the
415 request. Normally this is only needed when not using an HTTP CONNECT
416 tunnel.
417 """
418 if self.proxy is None:
419 return False
421 return not connection_requires_http_tunnel(
422 self.proxy, self.proxy_config, parsed_url.scheme
423 )
425 def urlopen( # type: ignore[override]
426 self, method: str, url: str, redirect: bool = True, **kw: typing.Any
427 ) -> BaseHTTPResponse:
428 """
429 Same as :meth:`urllib3.HTTPConnectionPool.urlopen`
430 with custom cross-host redirect logic and only sends the request-uri
431 portion of the ``url``.
433 The given ``url`` parameter must be absolute, such that an appropriate
434 :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
435 """
436 u = parse_url(url)
438 if u.scheme is None:
439 warnings.warn(
440 "URLs without a scheme (ie 'https://') are deprecated and will raise an error "
441 "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs "
442 "start with 'https://' or 'http://'. Read more in this issue: "
443 "https://github.com/urllib3/urllib3/issues/2920",
444 category=FutureWarning,
445 stacklevel=2,
446 )
448 conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
450 kw["assert_same_host"] = False
451 kw["redirect"] = False
453 if "headers" not in kw:
454 kw["headers"] = self.headers
456 if self._proxy_requires_url_absolute_form(u):
457 response = conn.urlopen(method, u._replace(fragment=None).url, **kw)
458 else:
459 response = conn.urlopen(method, u.request_uri, **kw)
461 redirect_location = redirect and response.get_redirect_location()
462 if not redirect_location:
463 return response
465 # Support relative URLs for redirecting.
466 redirect_location = urljoin(url, redirect_location)
468 if response.status == 303:
469 # Change the method according to RFC 9110, Section 15.4.4.
470 method = "GET"
471 # And lose the body not to transfer anything sensitive.
472 kw["body"] = None
473 kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
475 retries = kw.get("retries", response.retries)
476 if not isinstance(retries, Retry):
477 retries = Retry.from_int(retries, redirect=redirect)
479 # Strip headers marked as unsafe to forward to the redirected location.
480 # Check remove_headers_on_redirect to avoid a potential network call within
481 # conn.is_same_host() which may use socket.gethostbyname() in the future.
482 if retries.remove_headers_on_redirect and not conn.is_same_host(
483 redirect_location
484 ):
485 new_headers = kw["headers"].copy()
486 for header in kw["headers"]:
487 if header.lower() in retries.remove_headers_on_redirect:
488 new_headers.pop(header, None)
489 kw["headers"] = new_headers
491 try:
492 retries = retries.increment(method, url, response=response, _pool=conn)
493 except MaxRetryError:
494 if retries.raise_on_redirect:
495 response.drain_conn()
496 raise
497 return response
499 kw["retries"] = retries
500 kw["redirect"] = redirect
502 log.info("Redirecting %s -> %s", url, redirect_location)
504 response.drain_conn()
505 return self.urlopen(method, redirect_location, **kw)
508class ProxyManager(PoolManager):
509 """
510 Behaves just like :class:`PoolManager`, but sends all requests through
511 the defined proxy, using the CONNECT method for HTTPS URLs.
513 :param proxy_url:
514 The URL of the proxy to be used.
516 :param proxy_headers:
517 A dictionary containing headers that will be sent to the proxy. In case
518 of HTTP they are being sent with each request, while in the
519 HTTPS/CONNECT case they are sent only once. Could be used for proxy
520 authentication.
522 :param proxy_ssl_context:
523 The proxy SSL context is used to establish the TLS connection to the
524 proxy when using HTTPS proxies.
526 :param use_forwarding_for_https:
527 (Defaults to False) If set to True will forward requests to the HTTPS
528 proxy to be made on behalf of the client instead of creating a TLS
529 tunnel via the CONNECT method. **Enabling this flag means that request
530 and response headers and content will be visible from the HTTPS proxy**
531 whereas tunneling keeps request and response headers and content
532 private. IP address, target hostname, SNI, and port are always visible
533 to an HTTPS proxy even when this flag is disabled.
535 :param proxy_assert_hostname:
536 The hostname of the certificate to verify against.
538 :param proxy_assert_fingerprint:
539 The fingerprint of the certificate to verify against.
541 Example:
543 .. code-block:: python
545 import urllib3
547 proxy = urllib3.ProxyManager("https://localhost:3128/")
549 resp1 = proxy.request("GET", "http://google.com/")
550 resp2 = proxy.request("GET", "http://httpbin.org/")
552 # One pool was shared by both plain HTTP requests.
553 print(len(proxy.pools))
554 # 1
556 resp3 = proxy.request("GET", "https://httpbin.org/")
557 resp4 = proxy.request("GET", "https://twitter.com/")
559 # A separate pool was added for each HTTPS target.
560 print(len(proxy.pools))
561 # 3
563 """
565 def __init__(
566 self,
567 proxy_url: str,
568 num_pools: int = 10,
569 headers: typing.Mapping[str, str] | None = None,
570 proxy_headers: typing.Mapping[str, str] | None = None,
571 proxy_ssl_context: ssl.SSLContext | None = None,
572 use_forwarding_for_https: bool = False,
573 proxy_assert_hostname: None | str | typing.Literal[False] = None,
574 proxy_assert_fingerprint: str | None = None,
575 **connection_pool_kw: typing.Any,
576 ) -> None:
577 if isinstance(proxy_url, HTTPConnectionPool):
578 str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}"
579 else:
580 str_proxy_url = proxy_url
581 proxy = parse_url(str_proxy_url)
583 if proxy.scheme not in ("http", "https"):
584 raise ProxySchemeUnknown(proxy.scheme)
586 if (
587 use_forwarding_for_https
588 and proxy.scheme == "https"
589 and connection_pool_kw.get("ssl_context") is not None
590 ):
591 warnings.warn(
592 "Passing ssl_context when use_forwarding_for_https=True is deprecated "
593 "and will raise an error in urllib3 v3.0. "
594 "Use proxy_ssl_context to configure the TLS connection to the proxy.",
595 FutureWarning,
596 stacklevel=2,
597 )
598 if proxy_ssl_context is None:
599 proxy_ssl_context = connection_pool_kw.get("ssl_context")
601 if proxy.port is None:
602 port = port_by_scheme.get(proxy.scheme, 80)
603 proxy = proxy._replace(port=port)
605 self.proxy = proxy
606 self.proxy_headers = proxy_headers or {}
607 self.proxy_ssl_context = proxy_ssl_context
608 self.proxy_config = ProxyConfig(
609 proxy_ssl_context,
610 use_forwarding_for_https,
611 proxy_assert_hostname,
612 proxy_assert_fingerprint,
613 )
615 connection_pool_kw["_proxy"] = self.proxy
616 connection_pool_kw["_proxy_headers"] = self.proxy_headers
617 connection_pool_kw["_proxy_config"] = self.proxy_config
619 super().__init__(num_pools, headers, **connection_pool_kw)
621 def connection_from_host(
622 self,
623 host: str | None,
624 port: int | None = None,
625 scheme: str | None = "http",
626 pool_kwargs: dict[str, typing.Any] | None = None,
627 ) -> HTTPConnectionPool:
628 if scheme == "https":
629 return super().connection_from_host(
630 host, port, scheme, pool_kwargs=pool_kwargs
631 )
633 return super().connection_from_host(
634 self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr]
635 )
637 def _set_proxy_headers(
638 self, url: str, headers: typing.Mapping[str, str] | None = None
639 ) -> typing.Mapping[str, str]:
640 """
641 Sets headers needed by proxies: specifically, the Accept and Host
642 headers. Only sets headers not provided by the user.
643 """
644 headers_ = {"Accept": "*/*"}
646 netloc = parse_url(url).netloc
647 if netloc:
648 headers_["Host"] = netloc
650 if headers:
651 headers_.update(headers)
652 return headers_
654 def urlopen( # type: ignore[override]
655 self, method: str, url: str, redirect: bool = True, **kw: typing.Any
656 ) -> BaseHTTPResponse:
657 "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
658 u = parse_url(url)
659 if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme):
660 # For connections using HTTP CONNECT, httplib sets the necessary
661 # headers on the CONNECT to the proxy. If we're not using CONNECT,
662 # we'll definitely need to set 'Host' at the very least.
663 headers = kw.get("headers", self.headers)
664 kw["headers"] = self._set_proxy_headers(url, headers)
666 return super().urlopen(method, url, redirect=redirect, **kw)
669def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager:
670 return ProxyManager(proxy_url=url, **kw)