8000 Add support for HTTPS proxies (available to trio/asyncio) (#745) · encode/httpcore@f30da8c · GitHub
[go: up one dir, main page]

Skip to content

Commit f30da8c

Browse files
Add support for HTTPS proxies (available to trio/asyncio) (#745)
* Add proxy_ssl_context argument * Add changelog * Document HTTPS proxies * Raise exception when proxy_ssl_context used with the http scheme * Update docs/proxies.md Co-authored-by: Tom Christie <tom@tomchristie.com> * Raise exception when TLS over TLS is used for sync stream * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: Tom Christie <tom@tomchristie.com>
1 parent b649bb0 commit f30da8c

File tree

5 files changed

+71
-5
lines changed

5 files changed

+71
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## Unreleased
88

9+
- Add support for HTTPS proxies. Currently only available for async. (#745)
910
- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762)
1011
- Handle HTTP/1.1 half-closed connections gracefully. (#641)
1112
- Drop Python 3.7 support. (#727)

docs/proxies.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,33 @@ proxy = httpcore.HTTPProxy(
5151
)
5252
```
5353

54-
## Proxy SSL and HTTP Versions
54+
## Proxy SSL
5555

56-
Proxy support currently only allows for HTTP/1.1 connections to the proxy,
57-
and does not currently support SSL proxy connections, which require HTTPS-in-HTTPS,
56+
The `httpcore` package also supports HTTPS proxies for http and https destinations.
57+
58+
HTTPS proxies can be used in the same way that HTTP proxies are.
59+
60+
```python
61+
proxy = httpcore.HTTPProxy(proxy_url="https://127.0.0.1:8080/")
62+
```
63+
64+
Also, when using HTTPS proxies, you may need to configure the SSL context, which you can do with the `proxy_ssl_context` argument.
65+
66+
```python
67+
import ssl
68+
import httpcore
69+
70+
proxy_ssl_context = ssl.create_default_context()
71+
proxy_ssl_context.check_hostname = False
72+
73+
proxy = httpcore.HTTPProxy('https://127.0.0.1:8080/', proxy_ssl_context=proxy_ssl_context)
74+
```
75+
76+
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.
77+
78+
## HTTP Versions
79+
80+
If you use proxies, keep in mind that the `httpcore` package only supports proxies to HTTP/1.1 servers.
5881

5982
## SOCKS proxy support
6083

httpcore/_async/http_proxy.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(
6464
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None,
6565
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
6666
ssl_context: Optional[ssl.SSLContext] = None,
67+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
6768
max_connections: Optional[int] = 10,
6869
max_keepalive_connections: Optional[int] = None,
6970
keepalive_expiry: Optional[float] = None,
@@ -88,6 +89,7 @@ def __init__(
8889
ssl_context: An SSL context to use for verifying connections.
8990
If not specified, the default `httpcore.default_ssl_context()`
9091
will be used.
92+
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
9193
max_connections: The maximum number of concurrent HTTP connections that
9294
the pool should allow. Any attempt to send a request on a pool that
9395
would exceed this amount will block until a connection is available.
@@ -122,8 +124,17 @@ def __init__(
122124
uds=uds,
123125
socket_options=socket_options,
124126
)
125-
self._ssl_context = ssl_context
127+
126128
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
129+
if (
130+
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
131+
): # pragma: no cover
132+
raise RuntimeError(
133+
"The `proxy_ssl_context` argument is not allowed for the http scheme"
134+
)
135+
136+
self._ssl_context = ssl_context
137+
self._proxy_ssl_context = proxy_ssl_context
127138
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
128139
if proxy_auth is not None:
129140
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
@@ -141,12 +152,14 @@ def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
141152
remote_origin=origin,
142153
keepalive_expiry=self._keepalive_expiry,
143154
network_backend=self._network_backend,
155+
proxy_ssl_context=self._proxy_ssl_context,
144156
)
145157
return AsyncTunnelHTTPConnection(
146158
proxy_origin=self._proxy_url.origin,
147159
proxy_headers=self._proxy_headers,
148160
remote_origin=origin,
149161
ssl_context=self._ssl_context,
162+
proxy_ssl_context=self._proxy_ssl_context,
150163
keepalive_expiry=self._keepalive_expiry,
151164
http1=self._http1,
152165
http2=self._http2,
@@ -163,12 +176,14 @@ def __init__(
163176
keepalive_expiry: Optional[float] = None,
164177
network_backend: Optional[AsyncNetworkBackend] = None,
165178
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
179+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
166180
) -> None:
167181
self._connection = AsyncHTTPConnection(
168182
origin=proxy_origin,
169183
keepalive_expiry=keepalive_expiry,
170184
network_backend=network_backend,
171185
socket_options=socket_options,
186+
ssl_context=proxy_ssl_context,
172187
)
173188
self._proxy_origin = proxy_origin
174189
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
@@ -222,6 +237,7 @@ def __init__(
222237
proxy_origin: Origin,
223238
remote_origin: Origin,
224239
ssl_context: Optional[ssl.SSLContext] = None,
240+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
225241
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
226242
keepalive_expiry: Optional[float] = None,
227243
http1: bool = True,
@@ -234,10 +250,12 @@ def __init__(
234250
keepalive_expiry=keepalive_expiry,
235251
network_backend=network_backend,
236252
socket_options=socket_options,
253+
ssl_context=proxy_ssl_context,
237254
)
238255
self._proxy_origin = proxy_origin
239256
self._remote_origin = remote_origin
240257
self._ssl_context = ssl_context
258+
self._proxy_ssl_context = proxy_ssl_context
241259
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
242260
self._keepalive_expiry = keepalive_expiry
243261
self._http1 = http1

httpcore/_backends/sync.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def start_tls(
4747
server_hostname: typing.Optional[str] = None,
4848
timeout: typing.Optional[float] = None,
4949
) -> NetworkStream:
50+
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
51+
raise RuntimeError(
52+
"Attempted to add a TLS layer on top of the existing "
53+
"TLS stream, which is not supported by httpcore package"
54+
)
55+
5056
exc_map: ExceptionMapping = {
5157
socket.timeout: ConnectTimeout,
5258
OSError: ConnectError,

httpcore/_sync/http_proxy.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(
6464
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None,
6565
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
6666
ssl_context: Optional[ssl.SSLContext] = None,
67+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
6768
max_connections: Optional[int] = 10,
6869
max_keepalive_connections: Optional[int] = None,
6970
keepalive_expiry: Optional[float] = None,
@@ -88,6 +89,7 @@ def __init__(
8889
ssl_context: An SSL context to use for verifying connections.
8990
If not specified, the default `httpcore.default_ssl_context()`
9091
will be used.
92+
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
9193
max_connections: The maximum number of concurrent HTTP connections that
9294
the pool should allow. Any attempt to send a request on a pool that
9395
would exceed this amount will block until a connection is available.
@@ -122,8 +124,17 @@ def __init__(
122124
uds=uds,
123125
socket_options=socket_options,
124126
)
125-
self._ssl_context = ssl_context
127+
126128
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
129+
if (
130+
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
131+
): # pragma: no cover
132+
raise RuntimeError(
133+
"The `proxy_ssl_context` argument is not allowed for the http scheme"
134+
)
135+
136+
self._ssl_context = ssl_context
137+
self._proxy_ssl_context = proxy_ssl_context
127138
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
128139
if proxy_auth is not None:
129140
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
@@ -141,12 +152,14 @@ def create_connection(self, origin: Origin) -> ConnectionInterface:
141152
remote_origin=origin,
142153
keepalive_expiry=self._keepalive_expiry,
143154
network_backend=self._network_backend,
155+
proxy_ssl_context=self._proxy_ssl_context,
144156
)
145157
return TunnelHTTPConnection(
146158
proxy_origin=self._proxy_url.origin,
147159
proxy_headers=self._proxy_headers,
148160
remote_origin=origin,
149161
ssl_context=self._ssl_context,
162+
proxy_ssl_context=self._proxy_ssl_context,
150163
keepalive_expiry=self._keepalive_expiry,
151164
http1=self._http1,
152165
http2=self._http2,
@@ -163,12 +176,14 @@ def __init__(
163176
keepalive_expiry: Optional[float] = None,
164177
network_backend: Optional[NetworkBackend] = None,
165178
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
179+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
166180
) -> None:
167181
self._connection = HTTPConnection(
168182
origin=proxy_origin,
169183
keepalive_expiry=keepalive_expiry,
170184
network_backend=network_backend,
171185
socket_options=socket_options,
186+
ssl_context=proxy_ssl_context,
172187
)
173188
self._proxy_origin = proxy_origin
174189
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
@@ -222,6 +237,7 @@ def __init__(
222237
proxy_origin: Origin,
223238
remote_origin: Origin,
224239
ssl_context: Optional[ssl.SSLContext] = None,
240+
proxy_ssl_context: Optional[ssl.SSLContext] = None,
225241
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
226242
keepalive_expiry: Optional[float] = None,
227243
http1: bool = True,
@@ -234,10 +250,12 @@ def __init__(
234250
keepalive_expiry=keepalive_expiry,
235251
network_backend=network_backend,
236252
socket_options=socket_options,
253+
ssl_context=proxy_ssl_context,
237254
)
238255
self._proxy_origin = proxy 179B _origin
239256
self._remote_origin = remote_origin
240257
self._ssl_context = ssl_context
258+
self._proxy_ssl_context = proxy_ssl_context
241259
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
242260
self._keepalive_expiry = keepalive_expiry
243261
self._http1 = http1

0 commit comments

Comments
 (0)
0