|
8 | 8 |
|
9 | 9 | import os.path |
10 | 10 | import socket # noqa: F401 |
| 11 | +import typing |
| 12 | +import warnings |
11 | 13 |
|
12 | 14 | from ..urllib3.exceptions import ClosedPoolError, ConnectTimeoutError |
13 | 15 | from ..urllib3.exceptions import HTTPError as _HTTPError |
|
25 | 27 | from ..urllib3.util import Timeout as TimeoutSauce |
26 | 28 | from ..urllib3.util import parse_url |
27 | 29 | from ..urllib3.util.retry import Retry |
| 30 | + |
28 | 31 | from .auth import _basic_auth_str |
29 | 32 | from .compat import basestring, urlparse |
30 | 33 | from .cookies import extract_cookies_to_jar |
@@ -60,12 +63,53 @@ def SOCKSProxyManager(*args, **kwargs): |
60 | 63 | raise InvalidSchema("Missing dependencies for SOCKS support.") |
61 | 64 |
|
62 | 65 |
|
| 66 | +if typing.TYPE_CHECKING: |
| 67 | + from .models import PreparedRequest |
| 68 | + |
| 69 | + |
63 | 70 | DEFAULT_POOLBLOCK = False |
64 | 71 | DEFAULT_POOLSIZE = 10 |
65 | 72 | DEFAULT_RETRIES = 0 |
66 | 73 | DEFAULT_POOL_TIMEOUT = None |
67 | 74 |
|
68 | 75 |
|
| 76 | +def _urllib3_request_context( |
| 77 | + request: "PreparedRequest", |
| 78 | + verify: "bool | str | None", |
| 79 | + client_cert: "typing.Tuple[str, str] | str | None", |
| 80 | + poolmanager: "PoolManager", |
| 81 | +) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])": |
| 82 | + host_params = {} |
| 83 | + pool_kwargs = {} |
| 84 | + parsed_request_url = urlparse(request.url) |
| 85 | + scheme = parsed_request_url.scheme.lower() |
| 86 | + port = parsed_request_url.port |
| 87 | + |
| 88 | + cert_reqs = "CERT_REQUIRED" |
| 89 | + if verify is False: |
| 90 | + cert_reqs = "CERT_NONE" |
| 91 | + elif isinstance(verify, str): |
| 92 | + if not os.path.isdir(verify): |
| 93 | + pool_kwargs["ca_certs"] = verify |
| 94 | + else: |
| 95 | + pool_kwargs["ca_cert_dir"] = verify |
| 96 | + pool_kwargs["cert_reqs"] = cert_reqs |
| 97 | + if client_cert is not None: |
| 98 | + if isinstance(client_cert, tuple) and len(client_cert) == 2: |
| 99 | + pool_kwargs["cert_file"] = client_cert[0] |
| 100 | + pool_kwargs["key_file"] = client_cert[1] |
| 101 | + else: |
| 102 | + # According to our docs, we allow users to specify just the client |
| 103 | + # cert path |
| 104 | + pool_kwargs["cert_file"] = client_cert |
| 105 | + host_params = { |
| 106 | + "scheme": scheme, |
| 107 | + "host": parsed_request_url.hostname, |
| 108 | + "port": port, |
| 109 | + } |
| 110 | + return host_params, pool_kwargs |
| 111 | + |
| 112 | + |
69 | 113 | class BaseAdapter: |
70 | 114 | """The Base Transport Adapter""" |
71 | 115 |
|
@@ -246,7 +290,6 @@ def cert_verify(self, conn, url, verify, cert): |
246 | 290 | :param cert: The SSL certificate to verify. |
247 | 291 | """ |
248 | 292 | if url.lower().startswith("https") and verify: |
249 | | - |
250 | 293 | cert_loc = None |
251 | 294 |
|
252 | 295 | # Allow self-specified cert location. |
@@ -327,15 +370,126 @@ def build_response(self, req, resp): |
327 | 370 |
|
328 | 371 | return response |
329 | 372 |
|
| 373 | + def build_connection_pool_key_attributes(self, request, verify, cert=None): |
| 374 | + """Build the PoolKey attributes used by urllib3 to return a connection. |
| 375 | +
|
| 376 | + This looks at the PreparedRequest, the user-specified verify value, |
| 377 | + and the value of the cert parameter to determine what PoolKey values |
| 378 | + to use to select a connection from a given urllib3 Connection Pool. |
| 379 | +
|
| 380 | + The SSL related pool key arguments are not consistently set. As of |
| 381 | + this writing, use the following to determine what keys may be in that |
| 382 | + dictionary: |
| 383 | +
|
| 384 | + * If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the |
| 385 | + default Requests SSL Context |
| 386 | + * If ``verify`` is ``False``, ``"ssl_context"`` will not be set but |
| 387 | + ``"cert_reqs"`` will be set |
| 388 | + * If ``verify`` is a string, (i.e., it is a user-specified trust bundle) |
| 389 | + ``"ca_certs"`` will be set if the string is not a directory recognized |
| 390 | + by :py:func:`os.path.isdir`, otherwise ``"ca_cert_dir"`` will be |
| 391 | + set. |
| 392 | + * If ``"cert"`` is specified, ``"cert_file"`` will always be set. If |
| 393 | + ``"cert"`` is a tuple with a second item, ``"key_file"`` will also |
| 394 | + be present |
| 395 | +
|
| 396 | + To override these settings, one may subclass this class, call this |
| 397 | + method and use the above logic to change parameters as desired. For |
| 398 | + example, if one wishes to use a custom :py:class:`ssl.SSLContext` one |
| 399 | + must both set ``"ssl_context"`` and based on what else they require, |
| 400 | + alter the other keys to ensure the desired behaviour. |
| 401 | +
|
| 402 | + :param request: |
| 403 | + The PreparedReqest being sent over the connection. |
| 404 | + :type request: |
| 405 | + :class:`~requests.models.PreparedRequest` |
| 406 | + :param verify: |
| 407 | + Either a boolean, in which case it controls whether |
| 408 | + we verify the server's TLS certificate, or a string, in which case it |
| 409 | + must be a path to a CA bundle to use. |
| 410 | + :param cert: |
| 411 | + (optional) Any user-provided SSL certificate for client |
| 412 | + authentication (a.k.a., mTLS). This may be a string (i.e., just |
| 413 | + the path to a file which holds both certificate and key) or a |
| 414 | + tuple of length 2 with the certificate file path and key file |
| 415 | + path. |
| 416 | + :returns: |
| 417 | + A tuple of two dictionaries. The first is the "host parameters" |
| 418 | + portion of the Pool Key including scheme, hostname, and port. The |
| 419 | + second is a dictionary of SSLContext related parameters. |
| 420 | + """ |
| 421 | + return _urllib3_request_context(request, verify, cert, self.poolmanager) |
| 422 | + |
| 423 | + def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None): |
| 424 | + """Returns a urllib3 connection for the given request and TLS settings. |
| 425 | + This should not be called from user code, and is only exposed for use |
| 426 | + when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. |
| 427 | +
|
| 428 | + :param request: |
| 429 | + The :class:`PreparedRequest <PreparedRequest>` object to be sent |
| 430 | + over the connection. |
| 431 | + :param verify: |
| 432 | + Either a boolean, in which case it controls whether we verify the |
| 433 | + server's TLS certificate, or a string, in which case it must be a |
| 434 | + path to a CA bundle to use. |
| 435 | + :param proxies: |
| 436 | + (optional) The proxies dictionary to apply to the request. |
| 437 | + :param cert: |
| 438 | + (optional) Any user-provided SSL certificate to be used for client |
| 439 | + authentication (a.k.a., mTLS). |
| 440 | + :rtype: |
| 441 | + urllib3.ConnectionPool |
| 442 | + """ |
| 443 | + proxy = select_proxy(request.url, proxies) |
| 444 | + try: |
| 445 | + host_params, pool_kwargs = self.build_connection_pool_key_attributes( |
| 446 | + request, |
| 447 | + verify, |
| 448 | + cert, |
| 449 | + ) |
| 450 | + except ValueError as e: |
| 451 | + raise InvalidURL(e, request=request) |
| 452 | + if proxy: |
| 453 | + proxy = prepend_scheme_if_needed(proxy, "http") |
| 454 | + proxy_url = parse_url(proxy) |
| 455 | + if not proxy_url.host: |
| 456 | + raise InvalidProxyURL( |
| 457 | + "Please check proxy URL. It is malformed " |
| 458 | + "and could be missing the host." |
| 459 | + ) |
| 460 | + proxy_manager = self.proxy_manager_for(proxy) |
| 461 | + conn = proxy_manager.connection_from_host( |
| 462 | + **host_params, pool_kwargs=pool_kwargs |
| 463 | + ) |
| 464 | + else: |
| 465 | + # Only scheme should be lower case |
| 466 | + conn = self.poolmanager.connection_from_host( |
| 467 | + **host_params, pool_kwargs=pool_kwargs |
| 468 | + ) |
| 469 | + |
| 470 | + return conn |
| 471 | + |
330 | 472 | def get_connection(self, url, proxies=None): |
331 | | - """Returns a urllib3 connection for the given URL. This should not be |
| 473 | + """DEPRECATED: Users should move to `get_connection_with_tls_context` |
| 474 | + for all subclasses of HTTPAdapter using Requests>=2.32.2. |
| 475 | +
|
| 476 | + Returns a urllib3 connection for the given URL. This should not be |
332 | 477 | called from user code, and is only exposed for use when subclassing the |
333 | 478 | :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. |
334 | 479 |
|
335 | 480 | :param url: The URL to connect to. |
336 | 481 | :param proxies: (optional) A Requests-style dictionary of proxies used on this request. |
337 | 482 | :rtype: urllib3.ConnectionPool |
338 | 483 | """ |
| 484 | + warnings.warn( |
| 485 | + ( |
| 486 | + "`get_connection` has been deprecated in favor of " |
| 487 | + "`get_connection_with_tls_context`. Custom HTTPAdapter subclasses " |
| 488 | + "will need to migrate for Requests>=2.32.2. Please see " |
| 489 | + "https://github.com/psf/requests/pull/6710 for more details." |
| 490 | + ), |
| 491 | + DeprecationWarning, |
| 492 | + ) |
339 | 493 | proxy = select_proxy(url, proxies) |
340 | 494 |
|
341 | 495 | if proxy: |
@@ -390,6 +544,9 @@ def request_url(self, request, proxies): |
390 | 544 | using_socks_proxy = proxy_scheme.startswith("socks") |
391 | 545 |
|
392 | 546 | url = request.path_url |
| 547 | + if url.startswith("//"): # Don't confuse urllib3 |
| 548 | + url = f"/{url.lstrip('/')}" |
| 549 | + |
393 | 550 | if is_proxied_http_request and not using_socks_proxy: |
394 | 551 | url = urldefragauth(request.url) |
395 | 552 |
|
@@ -450,7 +607,9 @@ def send( |
450 | 607 | """ |
451 | 608 |
|
452 | 609 | try: |
453 | | - conn = self.get_connection(request.url, proxies) |
| 610 | + conn = self.get_connection_with_tls_context( |
| 611 | + request, verify, proxies=proxies, cert=cert |
| 612 | + ) |
454 | 613 | except LocationValueError as e: |
455 | 614 | raise InvalidURL(e, request=request) |
456 | 615 |
|
|
0 commit comments