Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions httpie/compat.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os
import sys
from ssl import SSLContext
from typing import Any, Optional, Iterable

from requests.adapters import DEFAULT_CA_BUNDLE_PATH

from httpie.cookies import HTTPieCookiePolicy
from http import cookiejar # noqa

Expand Down Expand Up @@ -103,11 +106,26 @@ def get_dist_name(entry_point: importlib_metadata.EntryPoint) -> Optional[str]:

def ensure_default_certs_loaded(ssl_context: SSLContext) -> None:
"""
Workaround for a bug in Requests 2.32.3
Load the default CA certificates into the given SSL context.

Because we always use our own SSL context (to be able to support
`--ssl` and `--ciphers`), Requests doesn’t load any CA bundle for us
when `--verify=yes` — it assumes a custom context already trusts the
default CAs. See <https://github.com/httpie/cli/issues/1583>.

See <https://github.com/httpie/cli/issues/1583>
`SSLContext.load_default_certs()` relies on OpenSSL’s default verify
paths, which are empty on some platforms (most notably the python.org
macOS builds). In that case we fall back to the `certifi` bundle, which
is exactly what Requests itself trusts by default — otherwise HTTPie
would end up with an empty trust store and fail to verify any
certificate. See <https://github.com/httpie/cli/issues/1632>.

"""
if hasattr(ssl_context, 'load_default_certs'):
if not ssl_context.get_ca_certs():
ssl_context.load_default_certs()
if not hasattr(ssl_context, 'load_default_certs'):
return

if not ssl_context.get_ca_certs():
ssl_context.load_default_certs()

if not ssl_context.get_ca_certs() and os.path.exists(DEFAULT_CA_BUNDLE_PATH):
ssl_context.load_verify_locations(cafile=DEFAULT_CA_BUNDLE_PATH)
62 changes: 62 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import os
import ssl

from unittest import mock

from requests.adapters import DEFAULT_CA_BUNDLE_PATH

from httpie.compat import ensure_default_certs_loaded


def _empty_trust_store_env(tmp_path):
"""Point OpenSSL’s default verify paths at an empty location.

This emulates the python.org macOS builds, where the OS trust store is
not reachable through OpenSSL’s defaults.
"""
return {
'SSL_CERT_FILE': str(tmp_path / '__no_such_ca_bundle__.pem'),
'SSL_CERT_DIR': str(tmp_path),
}


def test_ensure_default_certs_loaded_from_os_trust_store():
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ensure_default_certs_loaded(ssl_context)
assert ssl_context.get_ca_certs()


def test_ensure_default_certs_loaded_falls_back_to_certifi(tmp_path):
"""<https://github.com/httpie/cli/issues/1632>"""
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
with mock.patch.dict(os.environ, _empty_trust_store_env(tmp_path)):
ssl_context.load_default_certs()
assert not ssl_context.get_ca_certs(), (
'precondition: the OS trust store must be empty for this test'
)
ensure_default_certs_loaded(ssl_context)
assert ssl_context.get_ca_certs(), (
'the certifi bundle should have been loaded as a fallback'
)


def test_ensure_default_certs_loaded_keeps_already_loaded_certs():
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations(cafile=DEFAULT_CA_BUNDLE_PATH)
already_loaded = ssl_context.get_ca_certs()
with mock.patch.object(ssl_context, 'load_default_certs') as load_default_certs:
ensure_default_certs_loaded(ssl_context)
load_default_certs.assert_not_called()
assert ssl_context.get_ca_certs() == already_loaded


def test_ensure_default_certs_loaded_without_any_bundle_available(tmp_path):
"""A missing certifi bundle must not turn into a hard error."""
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
with mock.patch.dict(os.environ, _empty_trust_store_env(tmp_path)), \
mock.patch(
'httpie.compat.DEFAULT_CA_BUNDLE_PATH',
str(tmp_path / '__no_such_certifi_bundle__.pem'),
):
ensure_default_certs_loaded(ssl_context)
assert not ssl_context.get_ca_certs()
Loading