diff --git a/httpcore/_async/connection.py b/httpcore/_async/connection.py index 2add4d857..335366a83 100644 --- a/httpcore/_async/connection.py +++ b/httpcore/_async/connection.py @@ -3,6 +3,7 @@ from .._backends.auto import AsyncBackend, AsyncLock, AsyncSocketStream, AutoBackend from .._exceptions import ConnectError, ConnectTimeout +from .._ssl import default_ssl_context from .._types import URL, Headers, Origin, TimeoutDict from .._utils import exponential_backoff, get_logger, url_to_origin from .base import AsyncByteStream, AsyncHTTPTransport, NewConnectionRequired @@ -33,7 +34,9 @@ def __init__( self._http2_enabled = http2 self._keepalive_expiry = keepalive_expiry self._uds = uds - self._ssl_context = SSLContext() if ssl_context is None else ssl_context + self._ssl_context = ( + default_ssl_context() if ssl_context is None else ssl_context + ) self.socket = socket self._local_address = local_address self._retries = retries diff --git a/httpcore/_async/connection_pool.py b/httpcore/_async/connection_pool.py index f86c2277c..a5f9c336a 100644 --- a/httpcore/_async/connection_pool.py +++ b/httpcore/_async/connection_pool.py @@ -15,6 +15,7 @@ from .._backends.auto import AsyncBackend, AsyncLock, AsyncSemaphore from .._backends.base import lookup_async_backend from .._exceptions import LocalProtocolError, PoolTimeout, UnsupportedProtocol +from .._ssl import default_ssl_context from .._threadlock import ThreadLock from .._types import URL, Headers, Origin, TimeoutDict from .._utils import get_logger, origin_to_url_string, url_to_origin @@ -125,7 +126,9 @@ def __init__( if isinstance(backend, str): backend = lookup_async_backend(backend) - self._ssl_context = SSLContext() if ssl_context is None else ssl_context + self._ssl_context = ( + default_ssl_context() if ssl_context is None else ssl_context + ) self._max_connections = max_connections self._max_keepalive_connections = max_keepalive_connections self._keepalive_expiry = keepalive_expiry diff --git a/httpcore/_async/http2.py b/httpcore/_async/http2.py index 35a4e0911..6fe778d75 100644 --- a/httpcore/_async/http2.py +++ b/httpcore/_async/http2.py @@ -51,7 +51,7 @@ def __init__( self._exhausted_available_stream_ids = False def __repr__(self) -> str: - return f"" + return f"" def info(self) -> str: return f"HTTP/2, {self._state.name}, {len(self._streams)} streams" diff --git a/httpcore/_backends/sync.py b/httpcore/_backends/sync.py index ee8f94b7e..2be28d658 100644 --- a/httpcore/_backends/sync.py +++ b/httpcore/_backends/sync.py @@ -156,12 +156,16 @@ def open_uds_stream( with map_exceptions(exc_map): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(connect_timeout) - sock.connect(path) - - if ssl_context is not None: - sock = ssl_context.wrap_socket( - sock, server_hostname=hostname.decode("ascii") - ) + try: + sock.connect(path) + + if ssl_context is not None: + sock = ssl_context.wrap_socket( + sock, server_hostname=hostname.decode("ascii") + ) + except BaseException: + sock.close() + raise return SyncSocketStream(sock=sock) diff --git a/httpcore/_ssl.py b/httpcore/_ssl.py new file mode 100644 index 000000000..c99c5a679 --- /dev/null +++ b/httpcore/_ssl.py @@ -0,0 +1,9 @@ +import ssl + +import certifi + + +def default_ssl_context() -> ssl.SSLContext: + context = ssl.create_default_context() + context.load_verify_locations(certifi.where()) + return context diff --git a/httpcore/_sync/connection.py b/httpcore/_sync/connection.py index 382a4f9f6..21653e8f5 100644 --- a/httpcore/_sync/connection.py +++ b/httpcore/_sync/connection.py @@ -3,6 +3,7 @@ from .._backends.sync import SyncBackend, SyncLock, SyncSocketStream, SyncBackend from .._exceptions import ConnectError, ConnectTimeout +from .._ssl import default_ssl_context from .._types import URL, Headers, Origin, TimeoutDict from .._utils import exponential_backoff, get_logger, url_to_origin from .base import SyncByteStream, SyncHTTPTransport, NewConnectionRequired @@ -33,7 +34,9 @@ def __init__( self._http2_enabled = http2 self._keepalive_expiry = keepalive_expiry self._uds = uds - self._ssl_context = SSLContext() if ssl_context is None else ssl_context + self._ssl_context = ( + default_ssl_context() if ssl_context is None else ssl_context + ) self.socket = socket self._local_address = local_address self._retries = retries diff --git a/httpcore/_sync/connection_pool.py b/httpcore/_sync/connection_pool.py index 22ca98fa4..356aad8b9 100644 --- a/httpcore/_sync/connection_pool.py +++ b/httpcore/_sync/connection_pool.py @@ -15,6 +15,7 @@ from .._backends.sync import SyncBackend, SyncLock, SyncSemaphore from .._backends.base import lookup_sync_backend from .._exceptions import LocalProtocolError, PoolTimeout, UnsupportedProtocol +from .._ssl import default_ssl_context from .._threadlock import ThreadLock from .._types import URL, Headers, Origin, TimeoutDict from .._utils import get_logger, origin_to_url_string, url_to_origin @@ -125,7 +126,9 @@ def __init__( if isinstance(backend, str): backend = lookup_sync_backend(backend) - self._ssl_context = SSLContext() if ssl_context is None else ssl_context + self._ssl_context = ( + default_ssl_context() if ssl_context is None else ssl_context + ) self._max_connections = max_connections self._max_keepalive_connections = max_keepalive_connections self._keepalive_expiry = keepalive_expiry diff --git a/httpcore/_sync/http2.py b/httpcore/_sync/http2.py index 90caf5faf..a6bd6ad13 100644 --- a/httpcore/_sync/http2.py +++ b/httpcore/_sync/http2.py @@ -51,7 +51,7 @@ def __init__( self._exhausted_available_stream_ids = False def __repr__(self) -> str: - return f"" + return f"" def info(self) -> str: return f"HTTP/2, {self._state.name}, {len(self._streams)} streams" diff --git a/requirements.txt b/requirements.txt index c8246d1a8..00333510f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,3 +38,4 @@ pytest-trio==0.7.0 pytest-asyncio==0.15.1 trustme==0.8.0 uvicorn==0.12.1; python_version < '3.7' +types-certifi==0.1.4 diff --git a/setup.cfg b/setup.cfg index 0e23b0624..fd215b5f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,9 @@ skip = httpcore/_sync/,tests/sync_tests/ [tool:pytest] addopts = -rxXs +filterwarnings = + error + default:::hypercorn markers = copied_from(source, changes=None): mark test as copied from somewhere else, along with a description of changes made to accodomate e.g. our test setup diff --git a/setup.py b/setup.py index 3f5648d16..689ae12a9 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def get_packages(package): packages=get_packages("httpcore"), include_package_data=True, zip_safe=False, - install_requires=["h11>=0.11,<0.13", "sniffio==1.*", "anyio==3.*"], + install_requires=["h11>=0.11,<0.13", "sniffio==1.*", "anyio==3.*", "certifi"], extras_require={ "http2": ["h2>=3,<5"], }, diff --git a/tests/async_tests/test_interfaces.py b/tests/async_tests/test_interfaces.py index ce547f820..3fc093eab 100644 --- a/tests/async_tests/test_interfaces.py +++ b/tests/async_tests/test_interfaces.py @@ -1,4 +1,5 @@ import platform +import ssl from typing import Tuple import pytest @@ -52,8 +53,12 @@ async def test_http_request(backend: str, server: Server) -> None: @pytest.mark.anyio -async def test_https_request(backend: str, https_server: Server) -> None: - async with httpcore.AsyncConnectionPool(backend=backend) as http: +async def test_https_request( + backend: str, https_server: Server, ssl_context: ssl.SSLContext +) -> None: + async with httpcore.AsyncConnectionPool( + backend=backend, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -92,8 +97,12 @@ async def test_request_unsupported_protocol( @pytest.mark.anyio -async def test_http2_request(backend: str, https_server: Server) -> None: - async with httpcore.AsyncConnectionPool(backend=backend, http2=True) as http: +async def test_http2_request( + backend: str, https_server: Server, ssl_context: ssl.SSLContext +) -> None: + async with httpcore.AsyncConnectionPool( + backend=backend, http2=True, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -173,9 +182,11 @@ async def test_http_request_reuse_connection(backend: str, server: Server) -> No @pytest.mark.anyio async def test_https_request_reuse_connection( - backend: str, https_server: Server + backend: str, https_server: Server, ssl_context: ssl.SSLContext ) -> None: - async with httpcore.AsyncConnectionPool(backend=backend) as http: + async with httpcore.AsyncConnectionPool( + backend=backend, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -291,11 +302,6 @@ async def test_http_proxy( @pytest.mark.parametrize("proxy_mode", ["DEFAULT", "FORWARD_ONLY", "TUNNEL_ONLY"]) @pytest.mark.parametrize("protocol,port", [(b"http", 80), (b"https", 443)]) @pytest.mark.trio -# Filter out ssl module deprecation warnings and asyncio module resource warning, -# convert other warnings to errors. -@pytest.mark.filterwarnings("ignore:.*(SSLContext|PROTOCOL_TLS):DeprecationWarning") -@pytest.mark.filterwarnings("ignore::ResourceWarning:asyncio") -@pytest.mark.filterwarnings("error") async def test_proxy_socket_does_not_leak_when_the_connection_hasnt_been_added_to_pool( proxy_server: URL, server: Server, @@ -353,6 +359,7 @@ async def test_proxy_https_requests( proxy_mode: str, http2: bool, https_server: Server, + ssl_context: ssl.SSLContext, ) -> None: max_connections = 1 async with httpcore.AsyncHTTPProxy( @@ -360,6 +367,7 @@ async def test_proxy_https_requests( proxy_mode=proxy_mode, max_connections=max_connections, http2=http2, + ssl_context=ssl_context, ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", @@ -412,9 +420,13 @@ async def test_connection_pool_get_connection_info( expected_during_idle: dict, backend: str, https_server: Server, + ssl_context: ssl.SSLContext, ) -> None: async with httpcore.AsyncConnectionPool( - http2=http2, keepalive_expiry=keepalive_expiry, backend=backend + http2=http2, + keepalive_expiry=keepalive_expiry, + backend=backend, + ssl_context=ssl_context, ) as http: _, _, stream_1, _ = await http.handle_async_request( method=b"GET", diff --git a/tests/conftest.py b/tests/conftest.py index 0ba96fdc9..994f31c4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import contextlib import os +import ssl import threading import time import typing @@ -7,6 +8,7 @@ import pytest import trustme +from httpcore._ssl import default_ssl_context from httpcore._types import URL from .utils import HypercornServer, LiveServer, Server, http_proxy_server @@ -140,6 +142,19 @@ def localhost_cert_private_key_file( yield tmp +@pytest.fixture(scope="session") +def ssl_context( + cert_authority: trustme.CA, localhost_cert: trustme.LeafCert +) -> ssl.SSLContext: + ssl_context = default_ssl_context() + + if hypercorn is not None: + cert_authority.configure_trust(ssl_context) + localhost_cert.configure_cert(ssl_context) + + return ssl_context + + @pytest.fixture(scope="session") def https_server( localhost_cert_pem_file: str, localhost_cert_private_key_file: str diff --git a/tests/sync_tests/test_interfaces.py b/tests/sync_tests/test_interfaces.py index cc31ab345..d80ceba73 100644 --- a/tests/sync_tests/test_interfaces.py +++ b/tests/sync_tests/test_interfaces.py @@ -1,4 +1,5 @@ import platform +import ssl from typing import Tuple import pytest @@ -52,8 +53,12 @@ def test_http_request(backend: str, server: Server) -> None: -def test_https_request(backend: str, https_server: Server) -> None: - with httpcore.SyncConnectionPool(backend=backend) as http: +def test_https_request( + backend: str, https_server: Server, ssl_context: ssl.SSLContext +) -> None: + with httpcore.SyncConnectionPool( + backend=backend, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -92,8 +97,12 @@ def test_request_unsupported_protocol( -def test_http2_request(backend: str, https_server: Server) -> None: - with httpcore.SyncConnectionPool(backend=backend, http2=True) as http: +def test_http2_request( + backend: str, https_server: Server, ssl_context: ssl.SSLContext +) -> None: + with httpcore.SyncConnectionPool( + backend=backend, http2=True, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -173,9 +182,11 @@ def test_http_request_reuse_connection(backend: str, server: Server) -> None: def test_https_request_reuse_connection( - backend: str, https_server: Server + backend: str, https_server: Server, ssl_context: ssl.SSLContext ) -> None: - with httpcore.SyncConnectionPool(backend=backend) as http: + with httpcore.SyncConnectionPool( + backend=backend, ssl_context=ssl_context + ) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), @@ -291,11 +302,6 @@ def test_http_proxy( @pytest.mark.parametrize("proxy_mode", ["DEFAULT", "FORWARD_ONLY", "TUNNEL_ONLY"]) @pytest.mark.parametrize("protocol,port", [(b"http", 80), (b"https", 443)]) -# Filter out ssl module deprecation warnings and asyncio module resource warning, -# convert other warnings to errors. -@pytest.mark.filterwarnings("ignore:.*(SSLContext|PROTOCOL_TLS):DeprecationWarning") -@pytest.mark.filterwarnings("ignore::ResourceWarning:asyncio") -@pytest.mark.filterwarnings("error") def test_proxy_socket_does_not_leak_when_the_connection_hasnt_been_added_to_pool( proxy_server: URL, server: Server, @@ -353,6 +359,7 @@ def test_proxy_https_requests( proxy_mode: str, http2: bool, https_server: Server, + ssl_context: ssl.SSLContext, ) -> None: max_connections = 1 with httpcore.SyncHTTPProxy( @@ -360,6 +367,7 @@ def test_proxy_https_requests( proxy_mode=proxy_mode, max_connections=max_connections, http2=http2, + ssl_context=ssl_context, ) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", @@ -412,9 +420,13 @@ def test_connection_pool_get_connection_info( expected_during_idle: dict, backend: str, https_server: Server, + ssl_context: ssl.SSLContext, ) -> None: with httpcore.SyncConnectionPool( - http2=http2, keepalive_expiry=keepalive_expiry, backend=backend + http2=http2, + keepalive_expiry=keepalive_expiry, + backend=backend, + ssl_context=ssl_context, ) as http: _, _, stream_1, _ = http.handle_request( method=b"GET", diff --git a/tests/utils.py b/tests/utils.py index de3091255..f14836484 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -179,6 +179,7 @@ def http_proxy_server(proxy_host: str, proxy_port: int): finally: if proc is not None: proc.kill() + proc.communicate() @contextlib.contextmanager