Format: text/x-python
⬇ Download File (7.8 KB)
   1  from __future__ import annotations
   2  
   3  import functools
   4  import socket
   5  import ssl
   6  import sys
   7  import typing
   8  
   9  from .._exceptions import (
  10      ConnectError,
  11      ConnectTimeout,
  12      ExceptionMapping,
  13      ReadError,
  14      ReadTimeout,
  15      WriteError,
  16      WriteTimeout,
  17      map_exceptions,
  18  )
  19  from .._utils import is_socket_readable
  20  from .base import SOCKET_OPTION, NetworkBackend, NetworkStream
  21  
  22  
  23  class TLSinTLSStream(NetworkStream):  # pragma: no cover
  24      """
  25      Because the standard `SSLContext.wrap_socket` method does
  26      not work for `SSLSocket` objects, we need this class
  27      to implement TLS stream using an underlying `SSLObject`
  28      instance in order to support TLS on top of TLS.
  29      """
  30  
  31      # Defined in RFC 8449
  32      TLS_RECORD_SIZE = 16384
  33  
  34      def __init__(
  35          self,
  36          sock: socket.socket,
  37          ssl_context: ssl.SSLContext,
  38          server_hostname: str | None = None,
  39          timeout: float | None = None,
  40      ):
  41          self._sock = sock
  42          self._incoming = ssl.MemoryBIO()
  43          self._outgoing = ssl.MemoryBIO()
  44  
  45          self.ssl_obj = ssl_context.wrap_bio(
  46              incoming=self._incoming,
  47              outgoing=self._outgoing,
  48              server_hostname=server_hostname,
  49          )
  50  
  51          self._sock.settimeout(timeout)
  52          self._perform_io(self.ssl_obj.do_handshake)
  53  
  54      def _perform_io(
  55          self,
  56          func: typing.Callable[..., typing.Any],
  57      ) -> typing.Any:
  58          ret = None
  59  
  60          while True:
  61              errno = None
  62              try:
  63                  ret = func()
  64              except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
  65                  errno = e.errno
  66  
  67              self._sock.sendall(self._outgoing.read())
  68  
  69              if errno == ssl.SSL_ERROR_WANT_READ:
  70                  buf = self._sock.recv(self.TLS_RECORD_SIZE)
  71  
  72                  if buf:
  73                      self._incoming.write(buf)
  74                  else:
  75                      self._incoming.write_eof()
  76              if errno is None:
  77                  return ret
  78  
  79      def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
  80          exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
  81          with map_exceptions(exc_map):
  82              self._sock.settimeout(timeout)
  83              return typing.cast(
  84                  bytes, self._perform_io(functools.partial(self.ssl_obj.read, max_bytes))
  85              )
  86  
  87      def write(self, buffer: bytes, timeout: float | None = None) -> None:
  88          exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
  89          with map_exceptions(exc_map):
  90              self._sock.settimeout(timeout)
  91              while buffer:
  92                  nsent = self._perform_io(functools.partial(self.ssl_obj.write, buffer))
  93                  buffer = buffer[nsent:]
  94  
  95      def close(self) -> None:
  96          self._sock.close()
  97  
  98      def start_tls(
  99          self,
 100          ssl_context: ssl.SSLContext,
 101          server_hostname: str | None = None,
 102          timeout: float | None = None,
 103      ) -> NetworkStream:
 104          raise NotImplementedError()
 105  
 106      def get_extra_info(self, info: str) -> typing.Any:
 107          if info == "ssl_object":
 108              return self.ssl_obj
 109          if info == "client_addr":
 110              return self._sock.getsockname()
 111          if info == "server_addr":
 112              return self._sock.getpeername()
 113          if info == "socket":
 114              return self._sock
 115          if info == "is_readable":
 116              return is_socket_readable(self._sock)
 117          return None
 118  
 119  
 120  class SyncStream(NetworkStream):
 121      def __init__(self, sock: socket.socket) -> None:
 122          self._sock = sock
 123  
 124      def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
 125          exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
 126          with map_exceptions(exc_map):
 127              self._sock.settimeout(timeout)
 128              return self._sock.recv(max_bytes)
 129  
 130      def write(self, buffer: bytes, timeout: float | None = None) -> None:
 131          if not buffer:
 132              return
 133  
 134          exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
 135          with map_exceptions(exc_map):
 136              while buffer:
 137                  self._sock.settimeout(timeout)
 138                  n = self._sock.send(buffer)
 139                  buffer = buffer[n:]
 140  
 141      def close(self) -> None:
 142          self._sock.close()
 143  
 144      def start_tls(
 145          self,
 146          ssl_context: ssl.SSLContext,
 147          server_hostname: str | None = None,
 148          timeout: float | None = None,
 149      ) -> NetworkStream:
 150          exc_map: ExceptionMapping = {
 151              socket.timeout: ConnectTimeout,
 152              OSError: ConnectError,
 153          }
 154          with map_exceptions(exc_map):
 155              try:
 156                  if isinstance(self._sock, ssl.SSLSocket):  # pragma: no cover
 157                      # If the underlying socket has already been upgraded
 158                      # to the TLS layer (i.e. is an instance of SSLSocket),
 159                      # we need some additional smarts to support TLS-in-TLS.
 160                      return TLSinTLSStream(
 161                          self._sock, ssl_context, server_hostname, timeout
 162                      )
 163                  else:
 164                      self._sock.settimeout(timeout)
 165                      sock = ssl_context.wrap_socket(
 166                          self._sock, server_hostname=server_hostname
 167                      )
 168              except Exception as exc:  # pragma: nocover
 169                  self.close()
 170                  raise exc
 171          return SyncStream(sock)
 172  
 173      def get_extra_info(self, info: str) -> typing.Any:
 174          if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket):
 175              return self._sock._sslobj  # type: ignore
 176          if info == "client_addr":
 177              return self._sock.getsockname()
 178          if info == "server_addr":
 179              return self._sock.getpeername()
 180          if info == "socket":
 181              return self._sock
 182          if info == "is_readable":
 183              return is_socket_readable(self._sock)
 184          return None
 185  
 186  
 187  class SyncBackend(NetworkBackend):
 188      def connect_tcp(
 189          self,
 190          host: str,
 191          port: int,
 192          timeout: float | None = None,
 193          local_address: str | None = None,
 194          socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
 195      ) -> NetworkStream:
 196          # Note that we automatically include `TCP_NODELAY`
 197          # in addition to any other custom socket options.
 198          if socket_options is None:
 199              socket_options = []  # pragma: no cover
 200          address = (host, port)
 201          source_address = None if local_address is None else (local_address, 0)
 202          exc_map: ExceptionMapping = {
 203              socket.timeout: ConnectTimeout,
 204              OSError: ConnectError,
 205          }
 206  
 207          with map_exceptions(exc_map):
 208              sock = socket.create_connection(
 209                  address,
 210                  timeout,
 211                  source_address=source_address,
 212              )
 213              for option in socket_options:
 214                  sock.setsockopt(*option)  # pragma: no cover
 215              sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 216          return SyncStream(sock)
 217  
 218      def connect_unix_socket(
 219          self,
 220          path: str,
 221          timeout: float | None = None,
 222          socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
 223      ) -> NetworkStream:  # pragma: nocover
 224          if sys.platform == "win32":
 225              raise RuntimeError(
 226                  "Attempted to connect to a UNIX socket on a Windows system."
 227              )
 228          if socket_options is None:
 229              socket_options = []
 230  
 231          exc_map: ExceptionMapping = {
 232              socket.timeout: ConnectTimeout,
 233              OSError: ConnectError,
 234          }
 235          with map_exceptions(exc_map):
 236              sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 237              for option in socket_options:
 238                  sock.setsockopt(*option)
 239              sock.settimeout(timeout)
 240              sock.connect(path)
 241          return SyncStream(sock)
 242