Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for HTTPS proxies (available to trio/asyncio) #745

Merged
merged 13 commits into from
Sep 1, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

- Add support for HTTPS proxies. Currently only available for async. (#745)
- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762)
- Handle HTTP/1.1 half-closed connections gracefully. (#641)
- Drop Python 3.7 support. (#727)
Expand Down
29 changes: 26 additions & 3 deletions docs/proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,33 @@ proxy = httpcore.HTTPProxy(
)
```

## Proxy SSL and HTTP Versions
## Proxy SSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs are really nice, thanks! 💚


Proxy support currently only allows for HTTP/1.1 connections to the proxy,
and does not currently support SSL proxy connections, which require HTTPS-in-HTTPS,
The `httpcore` package also supports HTTPS proxies for http and https destinations.

HTTPS proxies can be used in the same way that HTTP proxies are.

```python
proxy = httpcore.HTTPProxy(proxy_url="https://127.0.0.1:8080/")
```

Also, when using HTTPS proxies, you may need to configure the SSL context, which you can do with the `proxy_ssl_context` argument.

```python
import ssl
import httpcore

proxy_ssl_context = ssl.create_default_context()
proxy_ssl_context.check_hostname = False

proxy = httpcore.HTTPProxy('https://127.0.0.1:8080/', proxy_ssl_context=proxy_ssl_context)
```

It is important to note that the `ssl_context` argument is always used for the remote connection, and the `proxy_ssl_context` argument is always used for the proxy connection.

## HTTP Versions

If you use proxies, keep in mind that the `httpcore` package only supports proxies to HTTP/1.1 servers.

## SOCKS proxy support

Expand Down
20 changes: 19 additions & 1 deletion httpcore/_async/http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
ssl_context: Optional[ssl.SSLContext] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
max_connections: Optional[int] = 10,
max_keepalive_connections: Optional[int] = None,
keepalive_expiry: Optional[float] = None,
Expand All @@ -88,6 +89,7 @@ def __init__(
ssl_context: An SSL context to use for verifying connections.
If not specified, the default `httpcore.default_ssl_context()`
will be used.
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
max_connections: The maximum number of concurrent HTTP connections that
the pool should allow. Any attempt to send a request on a pool that
would exceed this amount will block until a connection is available.
Expand Down Expand Up @@ -122,8 +124,17 @@ def __init__(
uds=uds,
socket_options=socket_options,
)
self._ssl_context = ssl_context

self._proxy_url = enforce_url(proxy_url, name="proxy_url")
if (
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
): # pragma: no cover
raise RuntimeError(
"The `proxy_ssl_context` argument is not allowed for the http scheme"
)

self._ssl_context = ssl_context
self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
Expand All @@ -141,12 +152,14 @@ def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
remote_origin=origin,
keepalive_expiry=self._keepalive_expiry,
network_backend=self._network_backend,
proxy_ssl_context=self._proxy_ssl_context,
)
return AsyncTunnelHTTPConnection(
proxy_origin=self._proxy_url.origin,
proxy_headers=self._proxy_headers,
remote_origin=origin,
ssl_context=self._ssl_context,
proxy_ssl_context=self._proxy_ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
Expand All @@ -163,12 +176,14 @@ def __init__(
keepalive_expiry: Optional[float] = None,
network_backend: Optional[AsyncNetworkBackend] = None,
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
) -> None:
self._connection = AsyncHTTPConnection(
origin=proxy_origin,
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
Expand Down Expand Up @@ -222,6 +237,7 @@ def __init__(
proxy_origin: Origin,
remote_origin: Origin,
ssl_context: Optional[ssl.SSLContext] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
keepalive_expiry: Optional[float] = None,
http1: bool = True,
Expand All @@ -234,10 +250,12 @@ def __init__(
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._remote_origin = remote_origin
self._ssl_context = ssl_context
self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
Expand Down
6 changes: 6 additions & 0 deletions httpcore/_backends/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def start_tls(
server_hostname: typing.Optional[str] = None,
timeout: typing.Optional[float] = None,
) -> NetworkStream:
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
raise RuntimeError(
"Attempted to add a TLS layer on top of the existing "
"TLS stream, which is not supported by httpcore package"
)

exc_map: ExceptionMapping = {
socket.timeout: ConnectTimeout,
OSError: ConnectError,
Expand Down
20 changes: 19 additions & 1 deletion httpcore/_sync/http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
ssl_context: Optional[ssl.SSLContext] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
max_connections: Optional[int] = 10,
max_keepalive_connections: Optional[int] = None,
keepalive_expiry: Optional[float] = None,
Expand All @@ -88,6 +89,7 @@ def __init__(
ssl_context: An SSL context to use for verifying connections.
If not specified, the default `httpcore.default_ssl_context()`
will be used.
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
max_connections: The maximum number of concurrent HTTP connections that
the pool should allow. Any attempt to send a request on a pool that
would exceed this amount will block until a connection is available.
Expand Down Expand Up @@ -122,8 +124,17 @@ def __init__(
uds=uds,
socket_options=socket_options,
)
self._ssl_context = ssl_context

self._proxy_url = enforce_url(proxy_url, name="proxy_url")
if (
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
): # pragma: no cover
raise RuntimeError(
"The `proxy_ssl_context` argument is not allowed for the http scheme"
)

self._ssl_context = ssl_context
self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
Expand All @@ -141,12 +152,14 @@ def create_connection(self, origin: Origin) -> ConnectionInterface:
remote_origin=origin,
keepalive_expiry=self._keepalive_expiry,
network_backend=self._network_backend,
proxy_ssl_context=self._proxy_ssl_context,
)
return TunnelHTTPConnection(
proxy_origin=self._proxy_url.origin,
proxy_headers=self._proxy_headers,
remote_origin=origin,
ssl_context=self._ssl_context,
proxy_ssl_context=self._proxy_ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
Expand All @@ -163,12 +176,14 @@ def __init__(
keepalive_expiry: Optional[float] = None,
network_backend: Optional[NetworkBackend] = None,
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
) -> None:
self._connection = HTTPConnection(
origin=proxy_origin,
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
Expand Down Expand Up @@ -222,6 +237,7 @@ def __init__(
proxy_origin: Origin,
remote_origin: Origin,
ssl_context: Optional[ssl.SSLContext] = None,
proxy_ssl_context: Optional[ssl.SSLContext] = None,
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
keepalive_expiry: Optional[float] = None,
http1: bool = True,
Expand All @@ -234,10 +250,12 @@ def __init__(
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._remote_origin = remote_origin
self._ssl_context = ssl_context
self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
Expand Down