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

HTTPS proxy support #786

Merged
merged 19 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 16 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +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)
- Add support for HTTPS proxies. (#745, # 786)
- 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
114 changes: 110 additions & 4 deletions httpcore/_backends/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import ssl
import sys
import typing
from functools import partial

from .._exceptions import (
ConnectError,
Expand All @@ -17,6 +18,103 @@
from .base import SOCKET_OPTION, NetworkBackend, NetworkStream


class TLSinTLSStream(NetworkStream): # pragma: no cover
"""
Because the standard `SSLContext.wrap_socket` method does
not work for `SSLSocket` objects, we need this class
to implement TLS stream using an underlying `SSLObject`
instance in order to support TLS on top of TLS.
"""

# Defined in RFC 8449
TLS_RECORD_SIZE = 16384

def __init__(
self,
sock: socket.socket,
ssl_context: ssl.SSLContext,
server_hostname: typing.Optional[str] = None,
timeout: typing.Optional[float] = None,
):
self._sock = sock
self._incoming = ssl.MemoryBIO()
self._outgoing = ssl.MemoryBIO()

self.ssl_obj = ssl_context.wrap_bio(
incoming=self._incoming,
outgoing=self._outgoing,
server_hostname=server_hostname,
)

self._sock.settimeout(timeout)
self._perform_io(self.ssl_obj.do_handshake)

def _perform_io(
self,
func: typing.Callable[..., typing.Any],
) -> typing.Any:
ret = None

while True:
errno = None
try:
ret = func()
except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
errno = e.errno

self._sock.sendall(self._outgoing.read())

if errno == ssl.SSL_ERROR_WANT_READ:
buf = self._sock.recv(self.TLS_RECORD_SIZE)

if buf:
self._incoming.write(buf)
else:
self._incoming.write_eof() # pragma: no cover
tomchristie marked this conversation as resolved.
Show resolved Hide resolved
if errno is None:
return ret

def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes:
exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
with map_exceptions(exc_map):
self._sock.settimeout(timeout)
return typing.cast(
bytes, self._perform_io(partial(self.ssl_obj.read, max_bytes))
)

def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None:
exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
with map_exceptions(exc_map):
self._sock.settimeout(timeout)
while buffer:
nsent = self._perform_io(partial(self.ssl_obj.write, buffer))
buffer = buffer[nsent:]

def close(self) -> None:
self._sock.close()

def start_tls(
self,
ssl_context: ssl.SSLContext,
server_hostname: typing.Optional[str] = None,
timeout: typing.Optional[float] = None,
) -> "NetworkStream":
raise NotImplementedError()

def get_extra_info(self, info: str) -> typing.Any:
if info == "ssl_object":
return self.ssl_obj
if info == "client_addr":
return self._sock.getsockname()
if info == "server_addr":
return self._sock.getpeername()
if info == "socket":
return self._sock
if info == "is_readable":
return is_socket_readable(self._sock)
return None


class SyncStream(NetworkStream):
def __init__(self, sock: socket.socket) -> None:
self._sock = sock
Expand Down Expand Up @@ -59,10 +157,18 @@ def start_tls(
}
with map_exceptions(exc_map):
try:
self._sock.settimeout(timeout)
sock = ssl_context.wrap_socket(
self._sock, server_hostname=server_hostname
)
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
# If the underlying socket has already been upgraded
# to the TLS layer (i.e. is an instance of SSLSocket),
# we need some additional smarts to support TLS-in-TLS.
return TLSinTLSStream(
self._sock, ssl_context, server_hostname, timeout
)
else:
self._sock.settimeout(timeout)
sock = ssl_context.wrap_socket(
self._sock, server_hostname=server_hostname
)
except Exception as exc: # pragma: nocover
self.close()
raise exc
Expand Down