annotate urllib3/poolmanager.py @ 15:0a3943480712

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