Viewing file: sslproto.py (31 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# Contains code from https://github.com/MagicStack/uvloop/tree/v0.16.0 # SPDX-License-Identifier: PSF-2.0 AND (MIT OR Apache-2.0) # SPDX-FileCopyrightText: Copyright (c) 2015-2021 MagicStack Inc. http://magic.io
import collections import enum import warnings try: import ssl except ImportError: # pragma: no cover ssl = None
from . import constants from . import exceptions from . import protocols from . import transports from .log import logger
if ssl is not None: SSLAgainErrors = (ssl.SSLWantReadError, ssl.SSLSyscallError)
class SSLProtocolState(enum.Enum): UNWRAPPED = "UNWRAPPED" DO_HANDSHAKE = "DO_HANDSHAKE" WRAPPED = "WRAPPED" FLUSHING = "FLUSHING" SHUTDOWN = "SHUTDOWN"
class AppProtocolState(enum.Enum): # This tracks the state of app protocol (https://git.io/fj59P): # # INIT -cm-> CON_MADE [-dr*->] [-er-> EOF?] -cl-> CON_LOST # # * cm: connection_made() # * dr: data_received() # * er: eof_received() # * cl: connection_lost()
STATE_INIT = "STATE_INIT" STATE_CON_MADE = "STATE_CON_MADE" STATE_EOF = "STATE_EOF" STATE_CON_LOST = "STATE_CON_LOST"
def _create_transport_context(server_side, server_hostname): if server_side: raise ValueError('Server side SSL needs a valid SSLContext')
# Client side may pass ssl=True to use a default # context; in that case the sslcontext passed is None. # The default is secure for client connections. # Python 3.4+: use up-to-date strong settings. sslcontext = ssl.create_default_context() if not server_hostname: sslcontext.check_hostname = False return sslcontext
def add_flowcontrol_defaults(high, low, kb): if high is None: if low is None: hi = kb * 1024 else: lo = low hi = 4 * lo else: hi = high if low is None: lo = hi // 4 else: lo = low
if not hi >= lo >= 0: raise ValueError('high (%r) must be >= low (%r) must be >= 0' % (hi, lo))
return hi, lo
class _SSLProtocolTransport(transports._FlowControlMixin, transports.Transport):
_start_tls_compatible = True _sendfile_compatible = constants._SendfileMode.FALLBACK
def __init__(self, loop, ssl_protocol): self._loop = loop self._ssl_protocol = ssl_protocol self._closed = False
def get_extra_info(self, name, default=None): """Get optional transport information.""" return self._ssl_protocol._get_extra_info(name, default)
def set_protocol(self, protocol): self._ssl_protocol._set_app_protocol(protocol)
def get_protocol(self): return self._ssl_protocol._app_protocol
def is_closing(self): return self._closed
def close(self): """Close the transport.
Buffered data will be flushed asynchronously. No more data will be received. After all buffered data is flushed, the protocol's connection_lost() method will (eventually) called with None as its argument. """ if not self._closed: self._closed = True self._ssl_protocol._start_shutdown() else: self._ssl_protocol = None
def __del__(self, _warnings=warnings): if not self._closed: self._closed = True _warnings.warn( "unclosed transport <asyncio._SSLProtocolTransport " "object>", ResourceWarning)
def is_reading(self): return not self._ssl_protocol._app_reading_paused
def pause_reading(self): """Pause the receiving end.
No data will be passed to the protocol's data_received() method until resume_reading() is called. """ self._ssl_protocol._pause_reading()
def resume_reading(self): """Resume the receiving end.
Data received will once again be passed to the protocol's data_received() method. """ self._ssl_protocol._resume_reading()
def set_write_buffer_limits(self, high=None, low=None): """Set the high- and low-water limits for write flow control.
These two values control when to call the protocol's pause_writing() and resume_writing() methods. If specified, the low-water limit must be less than or equal to the high-water limit. Neither value can be negative.
The defaults are implementation-specific. If only the high-water limit is given, the low-water limit defaults to an implementation-specific value less than or equal to the high-water limit. Setting high to zero forces low to zero as well, and causes pause_writing() to be called whenever the buffer becomes non-empty. Setting low to zero causes resume_writing() to be called only once the buffer is empty. Use of zero for either limit is generally sub-optimal as it reduces opportunities for doing I/O and computation concurrently. """ self._ssl_protocol._set_write_buffer_limits(high, low) self._ssl_protocol._control_app_writing()
def get_write_buffer_limits(self): return (self._ssl_protocol._outgoing_low_water, self._ssl_protocol._outgoing_high_water)
def get_write_buffer_size(self): """Return the current size of the write buffers.""" return self._ssl_protocol._get_write_buffer_size()
def set_read_buffer_limits(self, high=None, low=None): """Set the high- and low-water limits for read flow control.
These two values control when to call the upstream transport's pause_reading() and resume_reading() methods. If specified, the low-water limit must be less than or equal to the high-water limit. Neither value can be negative.
The defaults are implementation-specific. If only the high-water limit is given, the low-water limit defaults to an implementation-specific value less than or equal to the high-water limit. Setting high to zero forces low to zero as well, and causes pause_reading() to be called whenever the buffer becomes non-empty. Setting low to zero causes resume_reading() to be called only once the buffer is empty. Use of zero for either limit is generally sub-optimal as it reduces opportunities for doing I/O and computation concurrently. """ self._ssl_protocol._set_read_buffer_limits(high, low) self._ssl_protocol._control_ssl_reading()
def get_read_buffer_limits(self): return (self._ssl_protocol._incoming_low_water, self._ssl_protocol._incoming_high_water)
def get_read_buffer_size(self): """Return the current size of the read buffer.""" return self._ssl_protocol._get_read_buffer_size()
@property def _protocol_paused(self): # Required for sendfile fallback pause_writing/resume_writing logic return self._ssl_protocol._app_writing_paused
def write(self, data): """Write some data bytes to the transport.
This does not block; it buffers the data and arranges for it to be sent out asynchronously. """ if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError(f"data: expecting a bytes-like instance, " f"got {type(data).__name__}") if not data: return self._ssl_protocol._write_appdata((data,))
def writelines(self, list_of_data): """Write a list (or any iterable) of data bytes to the transport.
The default implementation concatenates the arguments and calls write() on the result. """ self._ssl_protocol._write_appdata(list_of_data)
def write_eof(self): """Close the write end after flushing buffered data.
This raises :exc:`NotImplementedError` right now. """ raise NotImplementedError
def can_write_eof(self): """Return True if this transport supports write_eof(), False if not.""" return False
def abort(self): """Close the transport immediately.
Buffered data will be lost. No more data will be received. The protocol's connection_lost() method will (eventually) be called with None as its argument. """ self._force_close(None)
def _force_close(self, exc): self._closed = True if self._ssl_protocol is not None: self._ssl_protocol._abort(exc)
def _test__append_write_backlog(self, data): # for test only self._ssl_protocol._write_backlog.append(data) self._ssl_protocol._write_buffer_size += len(data)
class SSLProtocol(protocols.BufferedProtocol): max_size = 256 * 1024 # Buffer size passed to read()
_handshake_start_time = None _handshake_timeout_handle = None _shutdown_timeout_handle = None
def __init__(self, loop, app_protocol, sslcontext, waiter, server_side=False, server_hostname=None, call_connection_made=True, ssl_handshake_timeout=None, ssl_shutdown_timeout=None): if ssl is None: raise RuntimeError("stdlib ssl module not available")
self._ssl_buffer = bytearray(self.max_size) self._ssl_buffer_view = memoryview(self._ssl_buffer)
if ssl_handshake_timeout is None: ssl_handshake_timeout = constants.SSL_HANDSHAKE_TIMEOUT elif ssl_handshake_timeout <= 0: raise ValueError( f"ssl_handshake_timeout should be a positive number, " f"got {ssl_handshake_timeout}") if ssl_shutdown_timeout is None: ssl_shutdown_timeout = constants.SSL_SHUTDOWN_TIMEOUT elif ssl_shutdown_timeout <= 0: raise ValueError( f"ssl_shutdown_timeout should be a positive number, " f"got {ssl_shutdown_timeout}")
if not sslcontext: sslcontext = _create_transport_context( server_side, server_hostname)
self._server_side = server_side if server_hostname and not server_side: self._server_hostname = server_hostname else: self._server_hostname = None self._sslcontext = sslcontext # SSL-specific extra info. More info are set when the handshake # completes. self._extra = dict(sslcontext=sslcontext)
# App data write buffering self._write_backlog = collections.deque() self._write_buffer_size = 0
self._waiter = waiter self._loop = loop self._set_app_protocol(app_protocol) self._app_transport = None self._app_transport_created = False # transport, ex: SelectorSocketTransport self._transport = None self._ssl_handshake_timeout = ssl_handshake_timeout self._ssl_shutdown_timeout = ssl_shutdown_timeout # SSL and state machine self._incoming = ssl.MemoryBIO() self._outgoing = ssl.MemoryBIO() self._state = SSLProtocolState.UNWRAPPED self._conn_lost = 0 # Set when connection_lost called if call_connection_made: self._app_state = AppProtocolState.STATE_INIT else: self._app_state = AppProtocolState.STATE_CON_MADE self._sslobj = self._sslcontext.wrap_bio( self._incoming, self._outgoing, server_side=self._server_side, server_hostname=self._server_hostname)
# Flow Control
self._ssl_writing_paused = False
self._app_reading_paused = False
self._ssl_reading_paused = False self._incoming_high_water = 0 self._incoming_low_water = 0 self._set_read_buffer_limits() self._eof_received = False
self._app_writing_paused = False self._outgoing_high_water = 0 self._outgoing_low_water = 0 self._set_write_buffer_limits() self._get_app_transport()
def _set_app_protocol(self, app_protocol): self._app_protocol = app_protocol # Make fast hasattr check first if (hasattr(app_protocol, 'get_buffer') and isinstance(app_protocol, protocols.BufferedProtocol)): self._app_protocol_get_buffer = app_protocol.get_buffer self._app_protocol_buffer_updated = app_protocol.buffer_updated self._app_protocol_is_buffer = True else: self._app_protocol_is_buffer = False
def _wakeup_waiter(self, exc=None): if self._waiter is None: return if not self._waiter.cancelled(): if exc is not None: self._waiter.set_exception(exc) else: self._waiter.set_result(None) self._waiter = None
def _get_app_transport(self): if self._app_transport is None: if self._app_transport_created: raise RuntimeError('Creating _SSLProtocolTransport twice') self._app_transport = _SSLProtocolTransport(self._loop, self) self._app_transport_created = True return self._app_transport
def connection_made(self, transport): """Called when the low-level connection is made.
Start the SSL handshake. """ self._transport = transport self._start_handshake()
def connection_lost(self, exc): """Called when the low-level connection is lost or closed.
The argument is an exception object or None (the latter meaning a regular EOF is received or the connection was aborted or closed). """ self._write_backlog.clear() self._outgoing.read() self._conn_lost += 1
# Just mark the app transport as closed so that its __dealloc__ # doesn't complain. if self._app_transport is not None: self._app_transport._closed = True
if self._state != SSLProtocolState.DO_HANDSHAKE: if ( self._app_state == AppProtocolState.STATE_CON_MADE or self._app_state == AppProtocolState.STATE_EOF ): self._app_state = AppProtocolState.STATE_CON_LOST self._loop.call_soon(self._app_protocol.connection_lost, exc) self._set_state(SSLProtocolState.UNWRAPPED) self._transport = None self._app_transport = None self._app_protocol = None self._wakeup_waiter(exc)
if self._shutdown_timeout_handle: self._shutdown_timeout_handle.cancel() self._shutdown_timeout_handle = None if self._handshake_timeout_handle: self._handshake_timeout_handle.cancel() self._handshake_timeout_handle = None
def get_buffer(self, n): want = n if want <= 0 or want > self.max_size: want = self.max_size if len(self._ssl_buffer) < want: self._ssl_buffer = bytearray(want) self._ssl_buffer_view = memoryview(self._ssl_buffer) return self._ssl_buffer_view
def buffer_updated(self, nbytes): self._incoming.write(self._ssl_buffer_view[:nbytes])
if self._state == SSLProtocolState.DO_HANDSHAKE: self._do_handshake()
elif self._state == SSLProtocolState.WRAPPED: self._do_read()
elif self._state == SSLProtocolState.FLUSHING: self._do_flush()
elif self._state == SSLProtocolState.SHUTDOWN: self._do_shutdown()
def eof_received(self): """Called when the other end of the low-level stream is half-closed.
If this returns a false value (including None), the transport will close itself. If it returns a true value, closing the transport is up to the protocol. """ self._eof_received = True try: if self._loop.get_debug(): logger.debug("%r received EOF", self)
if self._state == SSLProtocolState.DO_HANDSHAKE: self._on_handshake_complete(ConnectionResetError)
elif self._state == SSLProtocolState.WRAPPED: self._set_state(SSLProtocolState.FLUSHING) if self._app_reading_paused: return True else: self._do_flush()
elif self._state == SSLProtocolState.FLUSHING: self._do_write() self._set_state(SSLProtocolState.SHUTDOWN) self._do_shutdown()
elif self._state == SSLProtocolState.SHUTDOWN: self._do_shutdown()
except Exception: self._transport.close() raise
def _get_extra_info(self, name, default=None): if name in self._extra: return self._extra[name] elif self._transport is not None: return self._transport.get_extra_info(name, default) else: return default
def _set_state(self, new_state): allowed = False
if new_state == SSLProtocolState.UNWRAPPED: allowed = True
elif ( self._state == SSLProtocolState.UNWRAPPED and new_state == SSLProtocolState.DO_HANDSHAKE ): allowed = True
elif ( self._state == SSLProtocolState.DO_HANDSHAKE and new_state == SSLProtocolState.WRAPPED ): allowed = True
elif ( self._state == SSLProtocolState.WRAPPED and new_state == SSLProtocolState.FLUSHING ): allowed = True
elif ( self._state == SSLProtocolState.FLUSHING and new_state == SSLProtocolState.SHUTDOWN ): allowed = True
if allowed: self._state = new_state
else: raise RuntimeError( 'cannot switch state from {} to {}'.format( self._state, new_state))
# Handshake flow
def _start_handshake(self): if self._loop.get_debug(): logger.debug("%r starts SSL handshake", self) self._handshake_start_time = self._loop.time() else: self._handshake_start_time = None
self._set_state(SSLProtocolState.DO_HANDSHAKE)
# start handshake timeout count down self._handshake_timeout_handle = \ self._loop.call_later(self._ssl_handshake_timeout, lambda: self._check_handshake_timeout())
self._do_handshake()
def _check_handshake_timeout(self): if self._state == SSLProtocolState.DO_HANDSHAKE: msg = ( f"SSL handshake is taking longer than " f"{self._ssl_handshake_timeout} seconds: " f"aborting the connection" ) self._fatal_error(ConnectionAbortedError(msg))
def _do_handshake(self): try: self._sslobj.do_handshake() except SSLAgainErrors: self._process_outgoing() except ssl.SSLError as exc: self._on_handshake_complete(exc) else: self._on_handshake_complete(None)
def _on_handshake_complete(self, handshake_exc): if self._handshake_timeout_handle is not None: self._handshake_timeout_handle.cancel() self._handshake_timeout_handle = None
sslobj = self._sslobj try: if handshake_exc is None: self._set_state(SSLProtocolState.WRAPPED) else: raise handshake_exc
peercert = sslobj.getpeercert() except Exception as exc: handshake_exc = None self._set_state(SSLProtocolState.UNWRAPPED) if isinstance(exc, ssl.CertificateError): msg = 'SSL handshake failed on verifying the certificate' else: msg = 'SSL handshake failed' self._fatal_error(exc, msg) self._wakeup_waiter(exc) return
if self._loop.get_debug(): dt = self._loop.time() - self._handshake_start_time logger.debug("%r: SSL handshake took %.1f ms", self, dt * 1e3)
# Add extra info that becomes available after handshake. self._extra.update(peercert=peercert, cipher=sslobj.cipher(), compression=sslobj.compression(), ssl_object=sslobj) if self._app_state == AppProtocolState.STATE_INIT: self._app_state = AppProtocolState.STATE_CON_MADE self._app_protocol.connection_made(self._get_app_transport()) self._wakeup_waiter() self._do_read()
# Shutdown flow
def _start_shutdown(self): if ( self._state in ( SSLProtocolState.FLUSHING, SSLProtocolState.SHUTDOWN, SSLProtocolState.UNWRAPPED ) ): return if self._app_transport is not None: self._app_transport._closed = True if self._state == SSLProtocolState.DO_HANDSHAKE: self._abort(None) else: self._set_state(SSLProtocolState.FLUSHING) self._shutdown_timeout_handle = self._loop.call_later( self._ssl_shutdown_timeout, lambda: self._check_shutdown_timeout() ) self._do_flush()
def _check_shutdown_timeout(self): if ( self._state in ( SSLProtocolState.FLUSHING, SSLProtocolState.SHUTDOWN ) ): self._transport._force_close( exceptions.TimeoutError('SSL shutdown timed out'))
def _do_flush(self): self._do_read() self._set_state(SSLProtocolState.SHUTDOWN) self._do_shutdown()
def _do_shutdown(self): try: if not self._eof_received: self._sslobj.unwrap() except SSLAgainErrors: self._process_outgoing() except ssl.SSLError as exc: self._on_shutdown_complete(exc) else: self._process_outgoing() self._call_eof_received() self._on_shutdown_complete(None)
def _on_shutdown_complete(self, shutdown_exc): if self._shutdown_timeout_handle is not None: self._shutdown_timeout_handle.cancel() self._shutdown_timeout_handle = None
if shutdown_exc: self._fatal_error(shutdown_exc) else: self._loop.call_soon(self._transport.close)
def _abort(self, exc): self._set_state(SSLProtocolState.UNWRAPPED) if self._transport is not None: self._transport._force_close(exc)
# Outgoing flow
def _write_appdata(self, list_of_data): if ( self._state in ( SSLProtocolState.FLUSHING, SSLProtocolState.SHUTDOWN, SSLProtocolState.UNWRAPPED ) ): if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: logger.warning('SSL connection is closed') self._conn_lost += 1 return
for data in list_of_data: self._write_backlog.append(data) self._write_buffer_size += len(data)
try: if self._state == SSLProtocolState.WRAPPED: self._do_write()
except Exception as ex: self._fatal_error(ex, 'Fatal error on SSL protocol')
def _do_write(self): try: while self._write_backlog: data = self._write_backlog[0] count = self._sslobj.write(data) data_len = len(data) if count < data_len: self._write_backlog[0] = data[count:] self._write_buffer_size -= count else: del self._write_backlog[0] self._write_buffer_size -= data_len except SSLAgainErrors: pass self._process_outgoing()
def _process_outgoing(self): if not self._ssl_writing_paused: data = self._outgoing.read() if len(data): self._transport.write(data) self._control_app_writing()
# Incoming flow
def _do_read(self): if ( self._state not in ( SSLProtocolState.WRAPPED, SSLProtocolState.FLUSHING, ) ): return try: if not self._app_reading_paused: if self._app_protocol_is_buffer: self._do_read__buffered() else: self._do_read__copied() if self._write_backlog: self._do_write() else: self._process_outgoing() self._control_ssl_reading() except Exception as ex: self._fatal_error(ex, 'Fatal error on SSL protocol')
def _do_read__buffered(self): offset = 0 count = 1
buf = self._app_protocol_get_buffer(self._get_read_buffer_size()) wants = len(buf)
try: count = self._sslobj.read(wants, buf)
if count > 0: offset = count while offset < wants: count = self._sslobj.read(wants - offset, buf[offset:]) if count > 0: offset += count else: break else: self._loop.call_soon(lambda: self._do_read()) except SSLAgainErrors: pass if offset > 0: self._app_protocol_buffer_updated(offset) if not count: # close_notify self._call_eof_received() self._start_shutdown()
def _do_read__copied(self): chunk = b'1' zero = True one = False
try: while True: chunk = self._sslobj.read(self.max_size) if not chunk: break if zero: zero = False one = True first = chunk elif one: one = False data = [first, chunk] else: data.append(chunk) except SSLAgainErrors: pass if one: self._app_protocol.data_received(first) elif not zero: self._app_protocol.data_received(b''.join(data)) if not chunk: # close_notify self._call_eof_received() self._start_shutdown()
def _call_eof_received(self): try: if self._app_state == AppProtocolState.STATE_CON_MADE: self._app_state = AppProtocolState.STATE_EOF keep_open = self._app_protocol.eof_received() if keep_open: logger.warning('returning true from eof_received() ' 'has no effect when using ssl') except (KeyboardInterrupt, SystemExit): raise except BaseException as ex: self._fatal_error(ex, 'Error calling eof_received()')
# Flow control for writes from APP socket
def _control_app_writing(self): size = self._get_write_buffer_size() if size >= self._outgoing_high_water and not self._app_writing_paused: self._app_writing_paused = True try: self._app_protocol.pause_writing() except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: self._loop.call_exception_handler({ 'message': 'protocol.pause_writing() failed', 'exception': exc, 'transport': self._app_transport, 'protocol': self, }) elif size <= self._outgoing_low_water and self._app_writing_paused: self._app_writing_paused = False try: self._app_protocol.resume_writing() except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: self._loop.call_exception_handler({ 'message': 'protocol.resume_writing() failed', 'exception': exc, 'transport': self._app_transport, 'protocol': self, })
def _get_write_buffer_size(self): return self._outgoing.pending + self._write_buffer_size
def _set_write_buffer_limits(self, high=None, low=None): high, low = add_flowcontrol_defaults( high, low, constants.FLOW_CONTROL_HIGH_WATER_SSL_WRITE) self._outgoing_high_water = high self._outgoing_low_water = low
# Flow control for reads to APP socket
def _pause_reading(self): self._app_reading_paused = True
def _resume_reading(self): if self._app_reading_paused: self._app_reading_paused = False
def resume(): if self._state == SSLProtocolState.WRAPPED: self._do_read() elif self._state == SSLProtocolState.FLUSHING: self._do_flush() elif self._state == SSLProtocolState.SHUTDOWN: self._do_shutdown() self._loop.call_soon(resume)
# Flow control for reads from SSL socket
def _control_ssl_reading(self): size = self._get_read_buffer_size() if size >= self._incoming_high_water and not self._ssl_reading_paused: self._ssl_reading_paused = True self._transport.pause_reading() elif size <= self._incoming_low_water and self._ssl_reading_paused: self._ssl_reading_paused = False self._transport.resume_reading()
def _set_read_buffer_limits(self, high=None, low=None): high, low = add_flowcontrol_defaults( high, low, constants.FLOW_CONTROL_HIGH_WATER_SSL_READ) self._incoming_high_water = high self._incoming_low_water = low
def _get_read_buffer_size(self): return self._incoming.pending
# Flow control for writes to SSL socket
def pause_writing(self): """Called when the low-level transport's buffer goes over the high-water mark. """ assert not self._ssl_writing_paused self._ssl_writing_paused = True
def resume_writing(self): """Called when the low-level transport's buffer drains below the low-water mark. """ assert self._ssl_writing_paused self._ssl_writing_paused = False self._process_outgoing()
def _fatal_error(self, exc, message='Fatal error on transport'): if self._transport: self._transport._force_close(exc)
if isinstance(exc, OSError): if self._loop.get_debug(): logger.debug("%r: %s", self, message, exc_info=True) elif not isinstance(exc, exceptions.CancelledError): self._loop.call_exception_handler({ 'message': message, 'exception': exc, 'transport': self._transport, 'protocol': self, })
|