Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 numberDiff line numberDiff 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)
- Handle `sni_hostname` extension with SOCKS proxy. (#774)
- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762)
- Handle HTTP/1.1 half-closed connections gracefully. (#641)
Expand Down
114 changes: 110 additions & 4 deletions httpcore/_backends/sync.py
Original file line numberDiff line numberDiff 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()
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 DownExpand 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