jpayne@7: from __future__ import annotations jpayne@7: jpayne@7: import functools jpayne@7: import logging jpayne@7: import typing jpayne@7: import warnings jpayne@7: from types import TracebackType jpayne@7: from urllib.parse import urljoin jpayne@7: jpayne@7: from ._collections import HTTPHeaderDict, RecentlyUsedContainer jpayne@7: from ._request_methods import RequestMethods jpayne@7: from .connection import ProxyConfig jpayne@7: from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme jpayne@7: from .exceptions import ( jpayne@7: LocationValueError, jpayne@7: MaxRetryError, jpayne@7: ProxySchemeUnknown, jpayne@7: URLSchemeUnknown, jpayne@7: ) jpayne@7: from .response import BaseHTTPResponse jpayne@7: from .util.connection import _TYPE_SOCKET_OPTIONS jpayne@7: from .util.proxy import connection_requires_http_tunnel jpayne@7: from .util.retry import Retry jpayne@7: from .util.timeout import Timeout jpayne@7: from .util.url import Url, parse_url jpayne@7: jpayne@7: if typing.TYPE_CHECKING: jpayne@7: import ssl jpayne@7: from typing import Literal jpayne@7: jpayne@7: __all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] jpayne@7: jpayne@7: jpayne@7: log = logging.getLogger(__name__) jpayne@7: jpayne@7: SSL_KEYWORDS = ( jpayne@7: "key_file", jpayne@7: "cert_file", jpayne@7: "cert_reqs", jpayne@7: "ca_certs", jpayne@7: "ca_cert_data", jpayne@7: "ssl_version", jpayne@7: "ssl_minimum_version", jpayne@7: "ssl_maximum_version", jpayne@7: "ca_cert_dir", jpayne@7: "ssl_context", jpayne@7: "key_password", jpayne@7: "server_hostname", jpayne@7: ) jpayne@7: # Default value for `blocksize` - a new parameter introduced to jpayne@7: # http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 jpayne@7: _DEFAULT_BLOCKSIZE = 16384 jpayne@7: jpayne@7: _SelfT = typing.TypeVar("_SelfT") jpayne@7: jpayne@7: jpayne@7: class PoolKey(typing.NamedTuple): jpayne@7: """ jpayne@7: All known keyword arguments that could be provided to the pool manager, its jpayne@7: pools, or the underlying connections. jpayne@7: jpayne@7: All custom key schemes should include the fields in this key at a minimum. jpayne@7: """ jpayne@7: jpayne@7: key_scheme: str jpayne@7: key_host: str jpayne@7: key_port: int | None jpayne@7: key_timeout: Timeout | float | int | None jpayne@7: key_retries: Retry | bool | int | None jpayne@7: key_block: bool | None jpayne@7: key_source_address: tuple[str, int] | None jpayne@7: key_key_file: str | None jpayne@7: key_key_password: str | None jpayne@7: key_cert_file: str | None jpayne@7: key_cert_reqs: str | None jpayne@7: key_ca_certs: str | None jpayne@7: key_ca_cert_data: str | bytes | None jpayne@7: key_ssl_version: int | str | None jpayne@7: key_ssl_minimum_version: ssl.TLSVersion | None jpayne@7: key_ssl_maximum_version: ssl.TLSVersion | None jpayne@7: key_ca_cert_dir: str | None jpayne@7: key_ssl_context: ssl.SSLContext | None jpayne@7: key_maxsize: int | None jpayne@7: key_headers: frozenset[tuple[str, str]] | None jpayne@7: key__proxy: Url | None jpayne@7: key__proxy_headers: frozenset[tuple[str, str]] | None jpayne@7: key__proxy_config: ProxyConfig | None jpayne@7: key_socket_options: _TYPE_SOCKET_OPTIONS | None jpayne@7: key__socks_options: frozenset[tuple[str, str]] | None jpayne@7: key_assert_hostname: bool | str | None jpayne@7: key_assert_fingerprint: str | None jpayne@7: key_server_hostname: str | None jpayne@7: key_blocksize: int | None jpayne@7: jpayne@7: jpayne@7: def _default_key_normalizer( jpayne@7: key_class: type[PoolKey], request_context: dict[str, typing.Any] jpayne@7: ) -> PoolKey: jpayne@7: """ jpayne@7: Create a pool key out of a request context dictionary. jpayne@7: jpayne@7: According to RFC 3986, both the scheme and host are case-insensitive. jpayne@7: Therefore, this function normalizes both before constructing the pool jpayne@7: key for an HTTPS request. If you wish to change this behaviour, provide jpayne@7: alternate callables to ``key_fn_by_scheme``. jpayne@7: jpayne@7: :param key_class: jpayne@7: The class to use when constructing the key. This should be a namedtuple jpayne@7: with the ``scheme`` and ``host`` keys at a minimum. jpayne@7: :type key_class: namedtuple jpayne@7: :param request_context: jpayne@7: A dictionary-like object that contain the context for a request. jpayne@7: :type request_context: dict jpayne@7: jpayne@7: :return: A namedtuple that can be used as a connection pool key. jpayne@7: :rtype: PoolKey jpayne@7: """ jpayne@7: # Since we mutate the dictionary, make a copy first jpayne@7: context = request_context.copy() jpayne@7: context["scheme"] = context["scheme"].lower() jpayne@7: context["host"] = context["host"].lower() jpayne@7: jpayne@7: # These are both dictionaries and need to be transformed into frozensets jpayne@7: for key in ("headers", "_proxy_headers", "_socks_options"): jpayne@7: if key in context and context[key] is not None: jpayne@7: context[key] = frozenset(context[key].items()) jpayne@7: jpayne@7: # The socket_options key may be a list and needs to be transformed into a jpayne@7: # tuple. jpayne@7: socket_opts = context.get("socket_options") jpayne@7: if socket_opts is not None: jpayne@7: context["socket_options"] = tuple(socket_opts) jpayne@7: jpayne@7: # Map the kwargs to the names in the namedtuple - this is necessary since jpayne@7: # namedtuples can't have fields starting with '_'. jpayne@7: for key in list(context.keys()): jpayne@7: context["key_" + key] = context.pop(key) jpayne@7: jpayne@7: # Default to ``None`` for keys missing from the context jpayne@7: for field in key_class._fields: jpayne@7: if field not in context: jpayne@7: context[field] = None jpayne@7: jpayne@7: # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context jpayne@7: if context.get("key_blocksize") is None: jpayne@7: context["key_blocksize"] = _DEFAULT_BLOCKSIZE jpayne@7: jpayne@7: return key_class(**context) jpayne@7: jpayne@7: jpayne@7: #: A dictionary that maps a scheme to a callable that creates a pool key. jpayne@7: #: This can be used to alter the way pool keys are constructed, if desired. jpayne@7: #: Each PoolManager makes a copy of this dictionary so they can be configured jpayne@7: #: globally here, or individually on the instance. jpayne@7: key_fn_by_scheme = { jpayne@7: "http": functools.partial(_default_key_normalizer, PoolKey), jpayne@7: "https": functools.partial(_default_key_normalizer, PoolKey), jpayne@7: } jpayne@7: jpayne@7: pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} jpayne@7: jpayne@7: jpayne@7: class PoolManager(RequestMethods): jpayne@7: """ jpayne@7: Allows for arbitrary requests while transparently keeping track of jpayne@7: necessary connection pools for you. jpayne@7: jpayne@7: :param num_pools: jpayne@7: Number of connection pools to cache before discarding the least jpayne@7: recently used pool. jpayne@7: jpayne@7: :param headers: jpayne@7: Headers to include with all requests, unless other headers are given jpayne@7: explicitly. jpayne@7: jpayne@7: :param \\**connection_pool_kw: jpayne@7: Additional parameters are used to create fresh jpayne@7: :class:`urllib3.connectionpool.ConnectionPool` instances. jpayne@7: jpayne@7: Example: jpayne@7: jpayne@7: .. code-block:: python jpayne@7: jpayne@7: import urllib3 jpayne@7: jpayne@7: http = urllib3.PoolManager(num_pools=2) jpayne@7: jpayne@7: resp1 = http.request("GET", "https://google.com/") jpayne@7: resp2 = http.request("GET", "https://google.com/mail") jpayne@7: resp3 = http.request("GET", "https://yahoo.com/") jpayne@7: jpayne@7: print(len(http.pools)) jpayne@7: # 2 jpayne@7: jpayne@7: """ jpayne@7: jpayne@7: proxy: Url | None = None jpayne@7: proxy_config: ProxyConfig | None = None jpayne@7: jpayne@7: def __init__( jpayne@7: self, jpayne@7: num_pools: int = 10, jpayne@7: headers: typing.Mapping[str, str] | None = None, jpayne@7: **connection_pool_kw: typing.Any, jpayne@7: ) -> None: jpayne@7: super().__init__(headers) jpayne@7: self.connection_pool_kw = connection_pool_kw jpayne@7: jpayne@7: self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] jpayne@7: self.pools = RecentlyUsedContainer(num_pools) jpayne@7: jpayne@7: # Locally set the pool classes and keys so other PoolManagers can jpayne@7: # override them. jpayne@7: self.pool_classes_by_scheme = pool_classes_by_scheme jpayne@7: self.key_fn_by_scheme = key_fn_by_scheme.copy() jpayne@7: jpayne@7: def __enter__(self: _SelfT) -> _SelfT: jpayne@7: return self jpayne@7: jpayne@7: def __exit__( jpayne@7: self, jpayne@7: exc_type: type[BaseException] | None, jpayne@7: exc_val: BaseException | None, jpayne@7: exc_tb: TracebackType | None, jpayne@7: ) -> Literal[False]: jpayne@7: self.clear() jpayne@7: # Return False to re-raise any potential exceptions jpayne@7: return False jpayne@7: jpayne@7: def _new_pool( jpayne@7: self, jpayne@7: scheme: str, jpayne@7: host: str, jpayne@7: port: int, jpayne@7: request_context: dict[str, typing.Any] | None = None, jpayne@7: ) -> HTTPConnectionPool: jpayne@7: """ jpayne@7: Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and jpayne@7: any additional pool keyword arguments. jpayne@7: jpayne@7: If ``request_context`` is provided, it is provided as keyword arguments jpayne@7: to the pool class used. This method is used to actually create the jpayne@7: connection pools handed out by :meth:`connection_from_url` and jpayne@7: companion methods. It is intended to be overridden for customization. jpayne@7: """ jpayne@7: pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] jpayne@7: if request_context is None: jpayne@7: request_context = self.connection_pool_kw.copy() jpayne@7: jpayne@7: # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly jpayne@7: # set to 'None' in the request_context. jpayne@7: if request_context.get("blocksize") is None: jpayne@7: request_context["blocksize"] = _DEFAULT_BLOCKSIZE jpayne@7: jpayne@7: # Although the context has everything necessary to create the pool, jpayne@7: # this function has historically only used the scheme, host, and port jpayne@7: # in the positional args. When an API change is acceptable these can jpayne@7: # be removed. jpayne@7: for key in ("scheme", "host", "port"): jpayne@7: request_context.pop(key, None) jpayne@7: jpayne@7: if scheme == "http": jpayne@7: for kw in SSL_KEYWORDS: jpayne@7: request_context.pop(kw, None) jpayne@7: jpayne@7: return pool_cls(host, port, **request_context) jpayne@7: jpayne@7: def clear(self) -> None: jpayne@7: """ jpayne@7: Empty our store of pools and direct them all to close. jpayne@7: jpayne@7: This will not affect in-flight connections, but they will not be jpayne@7: re-used after completion. jpayne@7: """ jpayne@7: self.pools.clear() jpayne@7: jpayne@7: def connection_from_host( jpayne@7: self, jpayne@7: host: str | None, jpayne@7: port: int | None = None, jpayne@7: scheme: str | None = "http", jpayne@7: pool_kwargs: dict[str, typing.Any] | None = None, jpayne@7: ) -> HTTPConnectionPool: jpayne@7: """ jpayne@7: Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. jpayne@7: jpayne@7: If ``port`` isn't given, it will be derived from the ``scheme`` using jpayne@7: ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is jpayne@7: provided, it is merged with the instance's ``connection_pool_kw`` jpayne@7: variable and used to create the new connection pool, if one is jpayne@7: needed. jpayne@7: """ jpayne@7: jpayne@7: if not host: jpayne@7: raise LocationValueError("No host specified.") jpayne@7: jpayne@7: request_context = self._merge_pool_kwargs(pool_kwargs) jpayne@7: request_context["scheme"] = scheme or "http" jpayne@7: if not port: jpayne@7: port = port_by_scheme.get(request_context["scheme"].lower(), 80) jpayne@7: request_context["port"] = port jpayne@7: request_context["host"] = host jpayne@7: jpayne@7: return self.connection_from_context(request_context) jpayne@7: jpayne@7: def connection_from_context( jpayne@7: self, request_context: dict[str, typing.Any] jpayne@7: ) -> HTTPConnectionPool: jpayne@7: """ jpayne@7: Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. jpayne@7: jpayne@7: ``request_context`` must at least contain the ``scheme`` key and its jpayne@7: value must be a key in ``key_fn_by_scheme`` instance variable. jpayne@7: """ jpayne@7: if "strict" in request_context: jpayne@7: warnings.warn( jpayne@7: "The 'strict' parameter is no longer needed on Python 3+. " jpayne@7: "This will raise an error in urllib3 v2.1.0.", jpayne@7: DeprecationWarning, jpayne@7: ) jpayne@7: request_context.pop("strict") jpayne@7: jpayne@7: scheme = request_context["scheme"].lower() jpayne@7: pool_key_constructor = self.key_fn_by_scheme.get(scheme) jpayne@7: if not pool_key_constructor: jpayne@7: raise URLSchemeUnknown(scheme) jpayne@7: pool_key = pool_key_constructor(request_context) jpayne@7: jpayne@7: return self.connection_from_pool_key(pool_key, request_context=request_context) jpayne@7: jpayne@7: def connection_from_pool_key( jpayne@7: self, pool_key: PoolKey, request_context: dict[str, typing.Any] jpayne@7: ) -> HTTPConnectionPool: jpayne@7: """ jpayne@7: Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. jpayne@7: jpayne@7: ``pool_key`` should be a namedtuple that only contains immutable jpayne@7: objects. At a minimum it must have the ``scheme``, ``host``, and jpayne@7: ``port`` fields. jpayne@7: """ jpayne@7: with self.pools.lock: jpayne@7: # If the scheme, host, or port doesn't match existing open jpayne@7: # connections, open a new ConnectionPool. jpayne@7: pool = self.pools.get(pool_key) jpayne@7: if pool: jpayne@7: return pool jpayne@7: jpayne@7: # Make a fresh ConnectionPool of the desired type jpayne@7: scheme = request_context["scheme"] jpayne@7: host = request_context["host"] jpayne@7: port = request_context["port"] jpayne@7: pool = self._new_pool(scheme, host, port, request_context=request_context) jpayne@7: self.pools[pool_key] = pool jpayne@7: jpayne@7: return pool jpayne@7: jpayne@7: def connection_from_url( jpayne@7: self, url: str, pool_kwargs: dict[str, typing.Any] | None = None jpayne@7: ) -> HTTPConnectionPool: jpayne@7: """ jpayne@7: Similar to :func:`urllib3.connectionpool.connection_from_url`. jpayne@7: jpayne@7: If ``pool_kwargs`` is not provided and a new pool needs to be jpayne@7: constructed, ``self.connection_pool_kw`` is used to initialize jpayne@7: the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` jpayne@7: is provided, it is used instead. Note that if a new pool does not jpayne@7: need to be created for the request, the provided ``pool_kwargs`` are jpayne@7: not used. jpayne@7: """ jpayne@7: u = parse_url(url) jpayne@7: return self.connection_from_host( jpayne@7: u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs jpayne@7: ) jpayne@7: jpayne@7: def _merge_pool_kwargs( jpayne@7: self, override: dict[str, typing.Any] | None jpayne@7: ) -> dict[str, typing.Any]: jpayne@7: """ jpayne@7: Merge a dictionary of override values for self.connection_pool_kw. jpayne@7: jpayne@7: This does not modify self.connection_pool_kw and returns a new dict. jpayne@7: Any keys in the override dictionary with a value of ``None`` are jpayne@7: removed from the merged dictionary. jpayne@7: """ jpayne@7: base_pool_kwargs = self.connection_pool_kw.copy() jpayne@7: if override: jpayne@7: for key, value in override.items(): jpayne@7: if value is None: jpayne@7: try: jpayne@7: del base_pool_kwargs[key] jpayne@7: except KeyError: jpayne@7: pass jpayne@7: else: jpayne@7: base_pool_kwargs[key] = value jpayne@7: return base_pool_kwargs jpayne@7: jpayne@7: def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: jpayne@7: """ jpayne@7: Indicates if the proxy requires the complete destination URL in the jpayne@7: request. Normally this is only needed when not using an HTTP CONNECT jpayne@7: tunnel. jpayne@7: """ jpayne@7: if self.proxy is None: jpayne@7: return False jpayne@7: jpayne@7: return not connection_requires_http_tunnel( jpayne@7: self.proxy, self.proxy_config, parsed_url.scheme jpayne@7: ) jpayne@7: jpayne@7: def urlopen( # type: ignore[override] jpayne@7: self, method: str, url: str, redirect: bool = True, **kw: typing.Any jpayne@7: ) -> BaseHTTPResponse: jpayne@7: """ jpayne@7: Same as :meth:`urllib3.HTTPConnectionPool.urlopen` jpayne@7: with custom cross-host redirect logic and only sends the request-uri jpayne@7: portion of the ``url``. jpayne@7: jpayne@7: The given ``url`` parameter must be absolute, such that an appropriate jpayne@7: :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. jpayne@7: """ jpayne@7: u = parse_url(url) jpayne@7: jpayne@7: if u.scheme is None: jpayne@7: warnings.warn( jpayne@7: "URLs without a scheme (ie 'https://') are deprecated and will raise an error " jpayne@7: "in a future version of urllib3. To avoid this DeprecationWarning ensure all URLs " jpayne@7: "start with 'https://' or 'http://'. Read more in this issue: " jpayne@7: "https://github.com/urllib3/urllib3/issues/2920", jpayne@7: category=DeprecationWarning, jpayne@7: stacklevel=2, jpayne@7: ) jpayne@7: jpayne@7: conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) jpayne@7: jpayne@7: kw["assert_same_host"] = False jpayne@7: kw["redirect"] = False jpayne@7: jpayne@7: if "headers" not in kw: jpayne@7: kw["headers"] = self.headers jpayne@7: jpayne@7: if self._proxy_requires_url_absolute_form(u): jpayne@7: response = conn.urlopen(method, url, **kw) jpayne@7: else: jpayne@7: response = conn.urlopen(method, u.request_uri, **kw) jpayne@7: jpayne@7: redirect_location = redirect and response.get_redirect_location() jpayne@7: if not redirect_location: jpayne@7: return response jpayne@7: jpayne@7: # Support relative URLs for redirecting. jpayne@7: redirect_location = urljoin(url, redirect_location) jpayne@7: jpayne@7: if response.status == 303: jpayne@7: # Change the method according to RFC 9110, Section 15.4.4. jpayne@7: method = "GET" jpayne@7: # And lose the body not to transfer anything sensitive. jpayne@7: kw["body"] = None jpayne@7: kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() jpayne@7: jpayne@7: retries = kw.get("retries") jpayne@7: if not isinstance(retries, Retry): jpayne@7: retries = Retry.from_int(retries, redirect=redirect) jpayne@7: jpayne@7: # Strip headers marked as unsafe to forward to the redirected location. jpayne@7: # Check remove_headers_on_redirect to avoid a potential network call within jpayne@7: # conn.is_same_host() which may use socket.gethostbyname() in the future. jpayne@7: if retries.remove_headers_on_redirect and not conn.is_same_host( jpayne@7: redirect_location jpayne@7: ): jpayne@7: new_headers = kw["headers"].copy() jpayne@7: for header in kw["headers"]: jpayne@7: if header.lower() in retries.remove_headers_on_redirect: jpayne@7: new_headers.pop(header, None) jpayne@7: kw["headers"] = new_headers jpayne@7: jpayne@7: try: jpayne@7: retries = retries.increment(method, url, response=response, _pool=conn) jpayne@7: except MaxRetryError: jpayne@7: if retries.raise_on_redirect: jpayne@7: response.drain_conn() jpayne@7: raise jpayne@7: return response jpayne@7: jpayne@7: kw["retries"] = retries jpayne@7: kw["redirect"] = redirect jpayne@7: jpayne@7: log.info("Redirecting %s -> %s", url, redirect_location) jpayne@7: jpayne@7: response.drain_conn() jpayne@7: return self.urlopen(method, redirect_location, **kw) jpayne@7: jpayne@7: jpayne@7: class ProxyManager(PoolManager): jpayne@7: """ jpayne@7: Behaves just like :class:`PoolManager`, but sends all requests through jpayne@7: the defined proxy, using the CONNECT method for HTTPS URLs. jpayne@7: jpayne@7: :param proxy_url: jpayne@7: The URL of the proxy to be used. jpayne@7: jpayne@7: :param proxy_headers: jpayne@7: A dictionary containing headers that will be sent to the proxy. In case jpayne@7: of HTTP they are being sent with each request, while in the jpayne@7: HTTPS/CONNECT case they are sent only once. Could be used for proxy jpayne@7: authentication. jpayne@7: jpayne@7: :param proxy_ssl_context: jpayne@7: The proxy SSL context is used to establish the TLS connection to the jpayne@7: proxy when using HTTPS proxies. jpayne@7: jpayne@7: :param use_forwarding_for_https: jpayne@7: (Defaults to False) If set to True will forward requests to the HTTPS jpayne@7: proxy to be made on behalf of the client instead of creating a TLS jpayne@7: tunnel via the CONNECT method. **Enabling this flag means that request jpayne@7: and response headers and content will be visible from the HTTPS proxy** jpayne@7: whereas tunneling keeps request and response headers and content jpayne@7: private. IP address, target hostname, SNI, and port are always visible jpayne@7: to an HTTPS proxy even when this flag is disabled. jpayne@7: jpayne@7: :param proxy_assert_hostname: jpayne@7: The hostname of the certificate to verify against. jpayne@7: jpayne@7: :param proxy_assert_fingerprint: jpayne@7: The fingerprint of the certificate to verify against. jpayne@7: jpayne@7: Example: jpayne@7: jpayne@7: .. code-block:: python jpayne@7: jpayne@7: import urllib3 jpayne@7: jpayne@7: proxy = urllib3.ProxyManager("https://localhost:3128/") jpayne@7: jpayne@7: resp1 = proxy.request("GET", "https://google.com/") jpayne@7: resp2 = proxy.request("GET", "https://httpbin.org/") jpayne@7: jpayne@7: print(len(proxy.pools)) jpayne@7: # 1 jpayne@7: jpayne@7: resp3 = proxy.request("GET", "https://httpbin.org/") jpayne@7: resp4 = proxy.request("GET", "https://twitter.com/") jpayne@7: jpayne@7: print(len(proxy.pools)) jpayne@7: # 3 jpayne@7: jpayne@7: """ jpayne@7: jpayne@7: def __init__( jpayne@7: self, jpayne@7: proxy_url: str, jpayne@7: num_pools: int = 10, jpayne@7: headers: typing.Mapping[str, str] | None = None, jpayne@7: proxy_headers: typing.Mapping[str, str] | None = None, jpayne@7: proxy_ssl_context: ssl.SSLContext | None = None, jpayne@7: use_forwarding_for_https: bool = False, jpayne@7: proxy_assert_hostname: None | str | Literal[False] = None, jpayne@7: proxy_assert_fingerprint: str | None = None, jpayne@7: **connection_pool_kw: typing.Any, jpayne@7: ) -> None: jpayne@7: if isinstance(proxy_url, HTTPConnectionPool): jpayne@7: str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" jpayne@7: else: jpayne@7: str_proxy_url = proxy_url jpayne@7: proxy = parse_url(str_proxy_url) jpayne@7: jpayne@7: if proxy.scheme not in ("http", "https"): jpayne@7: raise ProxySchemeUnknown(proxy.scheme) jpayne@7: jpayne@7: if not proxy.port: jpayne@7: port = port_by_scheme.get(proxy.scheme, 80) jpayne@7: proxy = proxy._replace(port=port) jpayne@7: jpayne@7: self.proxy = proxy jpayne@7: self.proxy_headers = proxy_headers or {} jpayne@7: self.proxy_ssl_context = proxy_ssl_context jpayne@7: self.proxy_config = ProxyConfig( jpayne@7: proxy_ssl_context, jpayne@7: use_forwarding_for_https, jpayne@7: proxy_assert_hostname, jpayne@7: proxy_assert_fingerprint, jpayne@7: ) jpayne@7: jpayne@7: connection_pool_kw["_proxy"] = self.proxy jpayne@7: connection_pool_kw["_proxy_headers"] = self.proxy_headers jpayne@7: connection_pool_kw["_proxy_config"] = self.proxy_config jpayne@7: jpayne@7: super().__init__(num_pools, headers, **connection_pool_kw) jpayne@7: jpayne@7: def connection_from_host( jpayne@7: self, jpayne@7: host: str | None, jpayne@7: port: int | None = None, jpayne@7: scheme: str | None = "http", jpayne@7: pool_kwargs: dict[str, typing.Any] | None = None, jpayne@7: ) -> HTTPConnectionPool: jpayne@7: if scheme == "https": jpayne@7: return super().connection_from_host( jpayne@7: host, port, scheme, pool_kwargs=pool_kwargs jpayne@7: ) jpayne@7: jpayne@7: return super().connection_from_host( jpayne@7: self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] jpayne@7: ) jpayne@7: jpayne@7: def _set_proxy_headers( jpayne@7: self, url: str, headers: typing.Mapping[str, str] | None = None jpayne@7: ) -> typing.Mapping[str, str]: jpayne@7: """ jpayne@7: Sets headers needed by proxies: specifically, the Accept and Host jpayne@7: headers. Only sets headers not provided by the user. jpayne@7: """ jpayne@7: headers_ = {"Accept": "*/*"} jpayne@7: jpayne@7: netloc = parse_url(url).netloc jpayne@7: if netloc: jpayne@7: headers_["Host"] = netloc jpayne@7: jpayne@7: if headers: jpayne@7: headers_.update(headers) jpayne@7: return headers_ jpayne@7: jpayne@7: def urlopen( # type: ignore[override] jpayne@7: self, method: str, url: str, redirect: bool = True, **kw: typing.Any jpayne@7: ) -> BaseHTTPResponse: jpayne@7: "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." jpayne@7: u = parse_url(url) jpayne@7: if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): jpayne@7: # For connections using HTTP CONNECT, httplib sets the necessary jpayne@7: # headers on the CONNECT to the proxy. If we're not using CONNECT, jpayne@7: # we'll definitely need to set 'Host' at the very least. jpayne@7: headers = kw.get("headers", self.headers) jpayne@7: kw["headers"] = self._set_proxy_headers(url, headers) jpayne@7: jpayne@7: return super().urlopen(method, url, redirect=redirect, **kw) jpayne@7: jpayne@7: jpayne@7: def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: jpayne@7: return ProxyManager(proxy_url=url, **kw)