Skip to content

Commit a42df6c

Browse files
HTTPS proxy support (#786)
* Add proxy_ssl_context argument * Add changelog * Add sync support for TLS-in-TLS connections * Add HTTPS proxy docs * Update CHANGELOG.md * Update httpcore/_sync/http_proxy.py * Update httpcore/_async/http_proxy.py * Update httpcore/_async/http_proxy.py * Update httpcore/_sync/http_proxy.py * Update httpcore/_backends/sync.py Co-authored-by: Kar Petrosyan <[email protected]> --------- Co-authored-by: karosis88 <[email protected]> Co-authored-by: Kar Petrosyan <[email protected]>
1 parent 56c0e4f commit a42df6c

File tree

2 files changed

+111
-5
lines changed

2 files changed

+111
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +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)
9+
- Add support for HTTPS proxies. (#745, # 786)
1010
- Handle `sni_hostname` extension with SOCKS proxy. (#774)
1111
- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762)
1212
- Handle HTTP/1.1 half-closed connections gracefully. (#641)

httpcore/_backends/sync.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import ssl
33
import sys
44
import typing
5+
from functools import partial
56

67
from .._exceptions import (
78
ConnectError,
@@ -17,6 +18,103 @@
1718
from .base import SOCKET_OPTION, NetworkBackend, NetworkStream
1819

1920

21+
class TLSinTLSStream(NetworkStream): # pragma: no cover
22+
"""
23+
Because the standard `SSLContext.wrap_socket` method does
24+
not work for `SSLSocket` objects, we need this class
25+
to implement TLS stream using an underlying `SSLObject`
26+
instance in order to support TLS on top of TLS.
27+
"""
28+
29+
# Defined in RFC 8449
30+
TLS_RECORD_SIZE = 16384
31+
32+
def __init__(
33+
self,
34+
sock: socket.socket,
35+
ssl_context: ssl.SSLContext,
36+
server_hostname: typing.Optional[str] = None,
37+
timeout: typing.Optional[float] = None,
38+
):
39+
self._sock = sock
40+
self._incoming = ssl.MemoryBIO()
41+
self._outgoing = ssl.MemoryBIO()
42+
43+
self.ssl_obj = ssl_context.wrap_bio(
44+
incoming=self._incoming,
45+
outgoing=self._outgoing,
46+
server_hostname=server_hostname,
47+
)
48+
49+
self._sock.settimeout(timeout)
50+
self._perform_io(self.ssl_obj.do_handshake)
51+
52+
def _perform_io(
53+
self,
54+
func: typing.Callable[..., typing.Any],
55+
) -> typing.Any:
56+
ret = None
57+
58+
while True:
59+
errno = None
60+
try:
61+
ret = func()
62+
except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
63+
errno = e.errno
64+
65+
self._sock.sendall(self._outgoing.read())
66+
67+
if errno == ssl.SSL_ERROR_WANT_READ:
68+
buf = self._sock.recv(self.TLS_RECORD_SIZE)
69+
70+
if buf:
71+
self._incoming.write(buf)
72+
else:
73+
self._incoming.write_eof()
74+
if errno is None:
75+
return ret
76+
77+
def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes:
78+
exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
79+
with map_exceptions(exc_map):
80+
self._sock.settimeout(timeout)
81+
return typing.cast(
82+
bytes, self._perform_io(partial(self.ssl_obj.read, max_bytes))
83+
)
84+
85+
def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None:
86+
exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
87+
with map_exceptions(exc_map):
88+
self._sock.settimeout(timeout)
89+
while buffer:
90+
nsent = self._perform_io(partial(self.ssl_obj.write, buffer))
91+
buffer = buffer[nsent:]
92+
93+
def close(self) -> None:
94+
self._sock.close()
95+
96+
def start_tls(
97+
self,
98+
ssl_context: ssl.SSLContext,
99+
server_hostname: typing.Optional[str] = None,
100+
timeout: typing.Optional[float] = None,
101+
) -> "NetworkStream":
102+
raise NotImplementedError()
103+
104+
def get_extra_info(self, info: str) -> typing.Any:
105+
if info == "ssl_object":
106+
return self.ssl_obj
107+
if info == "client_addr":
108+
return self._sock.getsockname()
109+
if info == "server_addr":
110+
return self._sock.getpeername()
111+
if info == "socket":
112+
return self._sock
113+
if info == "is_readable":
114+
return is_socket_readable(self._sock)
115+
return None
116+
117+
20118
class SyncStream(NetworkStream):
21119
def __init__(self, sock: socket.socket) -> None:
22120
self._sock = sock
@@ -59,10 +157,18 @@ def start_tls(
59157
}
60158
with map_exceptions(exc_map):
61159
try:
62-
self._sock.settimeout(timeout)
63-
sock = ssl_context.wrap_socket(
64-
self._sock, server_hostname=server_hostname
65-
)
160+
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
161+
# If the underlying socket has already been upgraded
162+
# to the TLS layer (i.e. is an instance of SSLSocket),
163+
# we need some additional smarts to support TLS-in-TLS.
164+
return TLSinTLSStream(
165+
self._sock, ssl_context, server_hostname, timeout
166+
)
167+
else:
168+
self._sock.settimeout(timeout)
169+
sock = ssl_context.wrap_socket(
170+
self._sock, server_hostname=server_hostname
171+
)
66172
except Exception as exc: # pragma: nocover
67173
self.close()
68174
raise exc

0 commit comments

Comments
 (0)