From 3a1c5e5f0950420acfdc5b4f9ed80161b55519ee Mon Sep 17 00:00:00 2001 From: DonnaIsDoughnut Date: Wed, 16 Sep 2026 15:57:24 +0300 Subject: [PATCH] Make SSL optional in compatibility/fetcher Interpreters built without SSL support (e.g. embedded / AOSP Python builds) don't define `urllib.request.HTTPSHandler` / `urllib2.HTTPSHandler` at all. Previously, importing pex.compatibility or pex.fetcher on such an interpreter would fail immediately. Now the SSL-specific imports are wrapped in try/except and a HAS_SSL flag tracks availability. URLFetcher only registers an HTTPSHandler when SSL is available, and eagerly initializing the default SSL context is skipped entirely in that case. Attempting to actually establish an HTTPS connection without SSL support now raises a clear IOError instead of failing at import time. --- pex/compatibility.py | 18 ++++++++++++++++-- pex/fetcher.py | 28 +++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pex/compatibility.py b/pex/compatibility.py index 787e07dc5..72e67ff68 100644 --- a/pex/compatibility.py +++ b/pex/compatibility.py @@ -134,7 +134,15 @@ def exec_function(ast, globals_map): from urllib.request import HTTPBasicAuthHandler as HTTPBasicAuthHandler from urllib.request import HTTPDigestAuthHandler as HTTPDigestAuthHandler from urllib.request import HTTPPasswordMgrWithDefaultRealm as HTTPPasswordMgrWithDefaultRealm - from urllib.request import HTTPSHandler as HTTPSHandler + + try: + # N.B.: Interpreters built without SSL support (as can happen for e.g. embedded / AOSP + # Python builds) do not define `urllib.request.HTTPSHandler` at all. We don't want a mere + # import of this module to blow up in that case; only code paths that actually need to + # perform an HTTPS fetch should fail (and only then, at the point of use). + from urllib.request import HTTPSHandler as HTTPSHandler + except ImportError: + HTTPSHandler = None # type: ignore[assignment,misc] from urllib.request import ProxyHandler as ProxyHandler from urllib.request import Request as Request from urllib.request import build_opener as build_opener @@ -153,11 +161,17 @@ def exec_function(ast, globals_map): from urllib2 import HTTPDigestAuthHandler as HTTPDigestAuthHandler from urllib2 import HTTPError as HTTPError from urllib2 import HTTPPasswordMgrWithDefaultRealm as HTTPPasswordMgrWithDefaultRealm - from urllib2 import HTTPSHandler as HTTPSHandler + + try: + from urllib2 import HTTPSHandler as HTTPSHandler + except ImportError: + HTTPSHandler = None # type: ignore[assignment,misc] from urllib2 import ProxyHandler as ProxyHandler from urllib2 import Request as Request from urllib2 import build_opener as build_opener +HAS_SSL = HTTPSHandler is not None + urlparse = _url_parse url_unquote = _url_unquote url_unquote_plus = _url_unquote_plus diff --git a/pex/fetcher.py b/pex/fetcher.py index b32a9cf9a..06ec7dc9c 100644 --- a/pex/fetcher.py +++ b/pex/fetcher.py @@ -13,6 +13,7 @@ from pex.auth import PasswordDatabase, PasswordEntry from pex.compatibility import ( + HAS_SSL, PY2, AbstractHTTPHandler, FileHandler, @@ -36,7 +37,7 @@ if TYPE_CHECKING: from ssl import SSLContext - from typing import Any, BinaryIO, Dict, Iterable, Iterator, Mapping, Optional, Text + from typing import Any, BinaryIO, Dict, Iterable, Iterator, List, Mapping, Optional, Text import attr # vendor:skip else: @@ -120,7 +121,16 @@ def create_ssl_context(self): # `from gevent import monkey; monkey.patch_all()` call. # # See: https://github.com/pex-tool/pex/issues/2415 - import ssl + try: + import ssl + except ImportError as e: + # The interpreter was built without SSL support (as can happen for e.g. embedded / + # AOSP Python builds). We only get here if something actually tries to establish an + # HTTPS connection; a plain import of this module never triggers this. + raise IOError( + "Cannot establish an HTTPS connection: this Python interpreter was built " + "without SSL support ({error}).".format(error=e) + ) ssl_context = ssl.create_default_context(cafile=self.cert) if self.client_cert: @@ -149,7 +159,12 @@ def initialize_ssl_context(network_configuration=None): # N.B.: We eagerly initialize an SSLContext for the default case of no CA cert and no client cert. # When a custom CA cert or client cert or both are configured, that code will need to call # initialize_ssl_context on its own. -initialize_ssl_context() +# +# This is skipped entirely when the interpreter has no SSL support at all (e.g. some embedded / +# AOSP Python builds): merely importing this module must not require SSL; only actually fetching +# an https:// URL should (and will, lazily, via create_ssl_context above). +if HAS_SSL: + initialize_ssl_context() class UnixHTTPConnection(HTTPConnection): @@ -254,9 +269,12 @@ def __init__( handlers = [ ProxyHandler(proxies), - HTTPSHandler(context=get_ssl_context(network_configuration=network_configuration)), UnixHTTPHandler(), - ] + ] # type: List[Any] + if HAS_SSL: + handlers.append( + HTTPSHandler(context=get_ssl_context(network_configuration=network_configuration)) + ) if handle_file_urls: handlers.append(FileHandler())