mirror of
https://github.com/httpie/cli.git
synced 2025-03-13 14:28:50 +01:00
Requests prior to 2.32.3 always loaded the default (system-wide) set of trusted certificates into custom SSL contexts. 2.32.3 no longer does. This has broken a lot of users, but the fix is moving slowly upstream due to security considerations - see https://github.com/psf/requests/issues/6730 and https://github.com/psf/requests/pull/6731 . As suggested at https://github.com/psf/requests/pull/6710#issuecomment-2137802782 this can be worked around by explicitly loading the default certificates into the context. We check the method exists before calling it just to be safe, it was added in Python 3.4. Signed-off-by: Adam Williamson <awilliam@redhat.com>
113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
import ssl
|
||
from typing import NamedTuple, Optional
|
||
|
||
from httpie.adapters import HTTPAdapter
|
||
# noinspection PyPackageRequirements
|
||
from urllib3.util.ssl_ import (
|
||
create_urllib3_context,
|
||
resolve_ssl_version,
|
||
)
|
||
|
||
|
||
SSL_VERSION_ARG_MAPPING = {
|
||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||
'ssl3': 'PROTOCOL_SSLv3',
|
||
'tls1': 'PROTOCOL_TLSv1',
|
||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||
}
|
||
AVAILABLE_SSL_VERSION_ARG_MAPPING = {
|
||
arg: getattr(ssl, constant_name)
|
||
for arg, constant_name in SSL_VERSION_ARG_MAPPING.items()
|
||
if hasattr(ssl, constant_name)
|
||
}
|
||
|
||
|
||
class HTTPieCertificate(NamedTuple):
|
||
cert_file: Optional[str] = None
|
||
key_file: Optional[str] = None
|
||
key_password: Optional[str] = None
|
||
|
||
def to_raw_cert(self):
|
||
"""Synthesize a requests-compatible (2-item tuple of cert and key file)
|
||
object from HTTPie's internal representation of a certificate."""
|
||
return (self.cert_file, self.key_file)
|
||
|
||
|
||
class HTTPieHTTPSAdapter(HTTPAdapter):
|
||
def __init__(
|
||
self,
|
||
verify: bool,
|
||
ssl_version: str = None,
|
||
ciphers: str = None,
|
||
**kwargs
|
||
):
|
||
self._ssl_context = self._create_ssl_context(
|
||
verify=verify,
|
||
ssl_version=ssl_version,
|
||
ciphers=ciphers,
|
||
)
|
||
# workaround for a bug in requests 2.32.3, see:
|
||
# https://github.com/httpie/cli/issues/1583
|
||
if getattr(self._ssl_context, 'load_default_certs', None) is not None:
|
||
# if load_default_certs is present, get_ca_certs must be
|
||
# also, no need for another getattr
|
||
if not self._ssl_context.get_ca_certs():
|
||
self._ssl_context.load_default_certs()
|
||
super().__init__(**kwargs)
|
||
|
||
def init_poolmanager(self, *args, **kwargs):
|
||
kwargs['ssl_context'] = self._ssl_context
|
||
return super().init_poolmanager(*args, **kwargs)
|
||
|
||
def proxy_manager_for(self, *args, **kwargs):
|
||
kwargs['ssl_context'] = self._ssl_context
|
||
return super().proxy_manager_for(*args, **kwargs)
|
||
|
||
def cert_verify(self, conn, url, verify, cert):
|
||
if isinstance(cert, HTTPieCertificate):
|
||
conn.key_password = cert.key_password
|
||
cert = cert.to_raw_cert()
|
||
|
||
return super().cert_verify(conn, url, verify, cert)
|
||
|
||
@staticmethod
|
||
def _create_ssl_context(
|
||
verify: bool,
|
||
ssl_version: str = None,
|
||
ciphers: str = None,
|
||
) -> 'ssl.SSLContext':
|
||
return create_urllib3_context(
|
||
ciphers=ciphers,
|
||
ssl_version=resolve_ssl_version(ssl_version),
|
||
# Since we are using a custom SSL context, we need to pass this
|
||
# here manually, even though it’s also passed to the connection
|
||
# in `super().cert_verify()`.
|
||
cert_reqs=ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
|
||
)
|
||
|
||
@classmethod
|
||
def get_default_ciphers_names(cls):
|
||
return [cipher['name'] for cipher in cls._create_ssl_context(verify=False).get_ciphers()]
|
||
|
||
|
||
def _is_key_file_encrypted(key_file):
|
||
"""Detects if a key file is encrypted or not.
|
||
|
||
Copy of the internal urllib function (urllib3.util.ssl_)"""
|
||
|
||
with open(key_file, "r") as f:
|
||
for line in f:
|
||
# Look for Proc-Type: 4,ENCRYPTED
|
||
if "ENCRYPTED" in line:
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
# We used to import the default set of TLS ciphers from urllib3, but they removed it.
|
||
# Instead, now urllib3 uses the list of ciphers configured by the system.
|
||
# <https://github.com/httpie/cli/pull/1501>
|
||
DEFAULT_SSL_CIPHERS_STRING = ':'.join(HTTPieHTTPSAdapter.get_default_ciphers_names())
|