You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
950 lines
35 KiB
950 lines
35 KiB
cdef _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 |
|
|
|
|
|
cdef class _SSLProtocolTransport: |
|
|
|
# TODO: |
|
# _sendfile_compatible = constants._SendfileMode.FALLBACK |
|
|
|
def __cinit__(self, Loop loop, ssl_protocol, context): |
|
self._loop = loop |
|
# SSLProtocol instance |
|
self._ssl_protocol = ssl_protocol |
|
self._closed = False |
|
if context is None: |
|
context = Context_CopyCurrent() |
|
self.context = context |
|
|
|
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. |
|
""" |
|
self._closed = True |
|
self._ssl_protocol._start_shutdown(self.context.copy()) |
|
|
|
def __dealloc__(self): |
|
if not self._closed: |
|
self._closed = True |
|
warnings_warn( |
|
"unclosed transport <uvloop.loop._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(self.context.copy()) |
|
|
|
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(self.context.copy()) |
|
|
|
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,), self.context.copy()) |
|
|
|
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, self.context.copy()) |
|
|
|
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 |
|
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) |
|
|
|
|
|
cdef class SSLProtocol: |
|
"""SSL protocol. |
|
|
|
Implementation of SSL on top of a socket using incoming and outgoing |
|
buffers which are ssl.MemoryBIO objects. |
|
""" |
|
|
|
def __cinit__(self, *args, **kwargs): |
|
self._ssl_buffer_len = SSL_READ_MAX_SIZE |
|
self._ssl_buffer = <char*>PyMem_RawMalloc(self._ssl_buffer_len) |
|
if not self._ssl_buffer: |
|
raise MemoryError() |
|
self._ssl_buffer_view = PyMemoryView_FromMemory( |
|
self._ssl_buffer, self._ssl_buffer_len, PyBUF_WRITE) |
|
|
|
def __dealloc__(self): |
|
self._ssl_buffer_view = None |
|
PyMem_RawFree(self._ssl_buffer) |
|
self._ssl_buffer = NULL |
|
self._ssl_buffer_len = 0 |
|
|
|
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_handshake_timeout is None: |
|
ssl_handshake_timeout = 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 = 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 = col_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._sslobj = None |
|
self._incoming = ssl_MemoryBIO() |
|
self._incoming_write = self._incoming.write |
|
self._outgoing = ssl_MemoryBIO() |
|
self._outgoing_read = self._outgoing.read |
|
self._state = UNWRAPPED |
|
self._conn_lost = 0 # Set when connection_lost called |
|
if call_connection_made: |
|
self._app_state = STATE_INIT |
|
else: |
|
self._app_state = STATE_CON_MADE |
|
|
|
# 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._app_writing_paused = False |
|
self._outgoing_high_water = 0 |
|
self._outgoing_low_water = 0 |
|
self._set_write_buffer_limits() |
|
|
|
cdef _set_app_protocol(self, app_protocol): |
|
self._app_protocol = app_protocol |
|
if (hasattr(app_protocol, 'get_buffer') and |
|
not isinstance(app_protocol, aio_Protocol)): |
|
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 |
|
|
|
cdef _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, context=None): |
|
if self._app_transport is None: |
|
if self._app_transport_created: |
|
raise RuntimeError('Creating _SSLProtocolTransport twice') |
|
self._app_transport = _SSLProtocolTransport(self._loop, self, |
|
context) |
|
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 != DO_HANDSHAKE: |
|
if self._app_state == STATE_CON_MADE or \ |
|
self._app_state == STATE_EOF: |
|
self._app_state = STATE_CON_LOST |
|
self._loop.call_soon(self._app_protocol.connection_lost, exc) |
|
self._set_state(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): |
|
cdef size_t want = n |
|
if want > SSL_READ_MAX_SIZE: |
|
want = SSL_READ_MAX_SIZE |
|
if self._ssl_buffer_len < want: |
|
self._ssl_buffer = <char*>PyMem_RawRealloc(self._ssl_buffer, want) |
|
if not self._ssl_buffer: |
|
raise MemoryError() |
|
self._ssl_buffer_len = want |
|
self._ssl_buffer_view = PyMemoryView_FromMemory( |
|
self._ssl_buffer, want, PyBUF_WRITE) |
|
return self._ssl_buffer_view |
|
|
|
def buffer_updated(self, nbytes): |
|
self._incoming_write(PyMemoryView_FromMemory( |
|
self._ssl_buffer, nbytes, PyBUF_WRITE)) |
|
|
|
if self._state == DO_HANDSHAKE: |
|
self._do_handshake() |
|
|
|
elif self._state == WRAPPED: |
|
self._do_read() |
|
|
|
elif self._state == FLUSHING: |
|
self._do_flush() |
|
|
|
elif self._state == 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. |
|
""" |
|
try: |
|
if self._loop.get_debug(): |
|
aio_logger.debug("%r received EOF", self) |
|
|
|
if self._state == DO_HANDSHAKE: |
|
self._on_handshake_complete(ConnectionResetError) |
|
|
|
elif self._state == WRAPPED or self._state == FLUSHING: |
|
# We treat a low-level EOF as a critical situation similar to a |
|
# broken connection - just send whatever is in the buffer and |
|
# close. No application level eof_received() is called - |
|
# because we don't want the user to think that this is a |
|
# graceful shutdown triggered by SSL "close_notify". |
|
self._set_state(SHUTDOWN) |
|
self._on_shutdown_complete(None) |
|
|
|
elif self._state == SHUTDOWN: |
|
self._on_shutdown_complete(None) |
|
|
|
except Exception: |
|
self._transport.close() |
|
raise |
|
|
|
cdef _get_extra_info(self, name, default=None): |
|
if name == 'uvloop.sslproto': |
|
return self |
|
elif 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 |
|
|
|
cdef _set_state(self, SSLProtocolState new_state): |
|
cdef bint allowed = False |
|
|
|
if new_state == UNWRAPPED: |
|
allowed = True |
|
|
|
elif self._state == UNWRAPPED and new_state == DO_HANDSHAKE: |
|
allowed = True |
|
|
|
elif self._state == DO_HANDSHAKE and new_state == WRAPPED: |
|
allowed = True |
|
|
|
elif self._state == WRAPPED and new_state == FLUSHING: |
|
allowed = True |
|
|
|
elif self._state == WRAPPED and new_state == SHUTDOWN: |
|
allowed = True |
|
|
|
elif self._state == FLUSHING and new_state == SHUTDOWN: |
|
allowed = True |
|
|
|
if allowed: |
|
self._state = new_state |
|
|
|
else: |
|
raise RuntimeError( |
|
'cannot switch state from {} to {}'.format( |
|
self._state, new_state)) |
|
|
|
# Handshake flow |
|
|
|
cdef _start_handshake(self): |
|
if self._loop.get_debug(): |
|
aio_logger.debug("%r starts SSL handshake", self) |
|
self._handshake_start_time = self._loop.time() |
|
else: |
|
self._handshake_start_time = None |
|
|
|
self._set_state(DO_HANDSHAKE) |
|
|
|
# start handshake timeout count down |
|
self._handshake_timeout_handle = \ |
|
self._loop.call_later(self._ssl_handshake_timeout, |
|
lambda: self._check_handshake_timeout()) |
|
|
|
try: |
|
self._sslobj = self._sslcontext.wrap_bio( |
|
self._incoming, self._outgoing, |
|
server_side=self._server_side, |
|
server_hostname=self._server_hostname) |
|
self._sslobj_read = self._sslobj.read |
|
self._sslobj_write = self._sslobj.write |
|
except Exception as ex: |
|
self._on_handshake_complete(ex) |
|
else: |
|
self._do_handshake() |
|
|
|
cdef _check_handshake_timeout(self): |
|
if self._state == 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)) |
|
|
|
cdef _do_handshake(self): |
|
try: |
|
self._sslobj.do_handshake() |
|
except ssl_SSLAgainErrors as exc: |
|
self._process_outgoing() |
|
except ssl_SSLError as exc: |
|
self._on_handshake_complete(exc) |
|
else: |
|
self._on_handshake_complete(None) |
|
|
|
cdef _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(WRAPPED) |
|
else: |
|
raise handshake_exc |
|
|
|
peercert = sslobj.getpeercert() |
|
except Exception as exc: |
|
self._set_state(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 |
|
aio_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 == STATE_INIT: |
|
self._app_state = STATE_CON_MADE |
|
self._app_protocol.connection_made(self._get_app_transport()) |
|
self._wakeup_waiter() |
|
|
|
# We should wakeup user code before sending the first data below. In |
|
# case of `start_tls()`, the user can only get the SSLTransport in the |
|
# wakeup callback, because `connection_made()` is not called again. |
|
# We should schedule the first data later than the wakeup callback so |
|
# that the user get a chance to e.g. check ALPN with the transport |
|
# before having to handle the first data. |
|
self._loop._call_soon_handle( |
|
new_MethodHandle(self._loop, |
|
"SSLProtocol._do_read", |
|
<method_t> self._do_read, |
|
None, # current context is good |
|
self)) |
|
|
|
# Shutdown flow |
|
|
|
cdef _start_shutdown(self, object context=None): |
|
if self._state in (FLUSHING, SHUTDOWN, UNWRAPPED): |
|
return |
|
# we don't need the context for _abort or the timeout, because |
|
# TCP transport._force_close() should be able to call |
|
# connection_lost() in the right context |
|
if self._app_transport is not None: |
|
self._app_transport._closed = True |
|
if self._state == DO_HANDSHAKE: |
|
self._abort(None) |
|
else: |
|
self._set_state(FLUSHING) |
|
self._shutdown_timeout_handle = \ |
|
self._loop.call_later(self._ssl_shutdown_timeout, |
|
lambda: self._check_shutdown_timeout()) |
|
self._do_flush(context) |
|
|
|
cdef _check_shutdown_timeout(self): |
|
if self._state in (FLUSHING, SHUTDOWN): |
|
self._transport._force_close( |
|
aio_TimeoutError('SSL shutdown timed out')) |
|
|
|
cdef _do_read_into_void(self, object context): |
|
"""Consume and discard incoming application data. |
|
|
|
If close_notify is received for the first time, call eof_received. |
|
""" |
|
cdef: |
|
bint close_notify = False |
|
try: |
|
while True: |
|
if not self._sslobj_read(SSL_READ_MAX_SIZE): |
|
close_notify = True |
|
break |
|
except ssl_SSLAgainErrors as exc: |
|
pass |
|
except ssl_SSLZeroReturnError: |
|
close_notify = True |
|
if close_notify: |
|
self._call_eof_received(context) |
|
|
|
cdef _do_flush(self, object context=None): |
|
"""Flush the write backlog, discarding new data received. |
|
|
|
We don't send close_notify in FLUSHING because we still want to send |
|
the remaining data over SSL, even if we received a close_notify. Also, |
|
no application-level resume_writing() or pause_writing() will be called |
|
in FLUSHING, as we could fully manage the flow control internally. |
|
""" |
|
try: |
|
self._do_read_into_void(context) |
|
self._do_write() |
|
self._process_outgoing() |
|
self._control_ssl_reading() |
|
except Exception as ex: |
|
self._on_shutdown_complete(ex) |
|
else: |
|
if not self._get_write_buffer_size(): |
|
self._set_state(SHUTDOWN) |
|
self._do_shutdown(context) |
|
|
|
cdef _do_shutdown(self, object context=None): |
|
"""Send close_notify and wait for the same from the peer.""" |
|
try: |
|
# we must skip all application data (if any) before unwrap |
|
self._do_read_into_void(context) |
|
try: |
|
self._sslobj.unwrap() |
|
except ssl_SSLAgainErrors as exc: |
|
self._process_outgoing() |
|
else: |
|
self._process_outgoing() |
|
if not self._get_write_buffer_size(): |
|
self._on_shutdown_complete(None) |
|
except Exception as ex: |
|
self._on_shutdown_complete(ex) |
|
|
|
cdef _on_shutdown_complete(self, shutdown_exc): |
|
if self._shutdown_timeout_handle is not None: |
|
self._shutdown_timeout_handle.cancel() |
|
self._shutdown_timeout_handle = None |
|
|
|
# we don't need the context here because TCP transport.close() should |
|
# be able to call connection_made() in the right context |
|
if shutdown_exc: |
|
self._fatal_error(shutdown_exc, 'Error occurred during shutdown') |
|
else: |
|
self._transport.close() |
|
|
|
cdef _abort(self, exc): |
|
self._set_state(UNWRAPPED) |
|
if self._transport is not None: |
|
self._transport._force_close(exc) |
|
|
|
# Outgoing flow |
|
|
|
cdef _write_appdata(self, list_of_data, object context): |
|
if self._state in (FLUSHING, SHUTDOWN, UNWRAPPED): |
|
if self._conn_lost >= LOG_THRESHOLD_FOR_CONNLOST_WRITES: |
|
aio_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 == WRAPPED: |
|
self._do_write() |
|
self._process_outgoing() |
|
self._control_app_writing(context) |
|
|
|
except Exception as ex: |
|
self._fatal_error(ex, 'Fatal error on SSL protocol') |
|
|
|
cdef _do_write(self): |
|
"""Do SSL write, consumes write backlog and fills outgoing BIO.""" |
|
cdef size_t data_len, count |
|
try: |
|
while self._write_backlog: |
|
data = self._write_backlog[0] |
|
count = self._sslobj_write(data) |
|
data_len = len(data) |
|
if count < data_len: |
|
if not PyMemoryView_Check(data): |
|
data = PyMemoryView_FromObject(data) |
|
self._write_backlog[0] = data[count:] |
|
self._write_buffer_size -= count |
|
else: |
|
del self._write_backlog[0] |
|
self._write_buffer_size -= data_len |
|
except ssl_SSLAgainErrors as exc: |
|
pass |
|
|
|
cdef _process_outgoing(self): |
|
"""Send bytes from the outgoing BIO.""" |
|
if not self._ssl_writing_paused: |
|
data = self._outgoing_read() |
|
if len(data): |
|
self._transport.write(data) |
|
|
|
# Incoming flow |
|
|
|
cdef _do_read(self): |
|
if self._state != WRAPPED: |
|
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() |
|
self._process_outgoing() |
|
self._control_app_writing() |
|
self._control_ssl_reading() |
|
except Exception as ex: |
|
self._fatal_error(ex, 'Fatal error on SSL protocol') |
|
|
|
cdef _do_read__buffered(self): |
|
cdef: |
|
Py_buffer pybuf |
|
bint pybuf_inited = False |
|
size_t wants, offset = 0 |
|
int count = 1 |
|
object buf |
|
|
|
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 |
|
if offset < wants: |
|
PyObject_GetBuffer(buf, &pybuf, PyBUF_WRITABLE) |
|
pybuf_inited = True |
|
while offset < wants: |
|
buf = PyMemoryView_FromMemory( |
|
(<char*>pybuf.buf) + offset, |
|
wants - offset, |
|
PyBUF_WRITE) |
|
count = self._sslobj_read(wants - offset, buf) |
|
if count > 0: |
|
offset += count |
|
else: |
|
break |
|
else: |
|
self._loop._call_soon_handle( |
|
new_MethodHandle(self._loop, |
|
"SSLProtocol._do_read", |
|
<method_t>self._do_read, |
|
None, # current context is good |
|
self)) |
|
except ssl_SSLAgainErrors as exc: |
|
pass |
|
finally: |
|
if pybuf_inited: |
|
PyBuffer_Release(&pybuf) |
|
if offset > 0: |
|
self._app_protocol_buffer_updated(offset) |
|
if not count: |
|
# close_notify |
|
self._call_eof_received() |
|
self._start_shutdown() |
|
|
|
cdef _do_read__copied(self): |
|
cdef: |
|
list data |
|
bytes first, chunk = b'1' |
|
bint zero = True, one = False |
|
|
|
try: |
|
while True: |
|
chunk = self._sslobj_read(SSL_READ_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 ssl_SSLAgainErrors as exc: |
|
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() |
|
|
|
cdef _call_eof_received(self, object context=None): |
|
if self._app_state == STATE_CON_MADE: |
|
self._app_state = STATE_EOF |
|
try: |
|
if context is None: |
|
# If the caller didn't provide a context, we assume the |
|
# caller is already in the right context, which is usually |
|
# inside the upstream callbacks like buffer_updated() |
|
keep_open = self._app_protocol.eof_received() |
|
else: |
|
keep_open = run_in_context( |
|
context, self._app_protocol.eof_received, |
|
) |
|
except (KeyboardInterrupt, SystemExit): |
|
raise |
|
except BaseException as ex: |
|
self._fatal_error(ex, 'Error calling eof_received()') |
|
else: |
|
if keep_open: |
|
aio_logger.warning('returning true from eof_received() ' |
|
'has no effect when using ssl') |
|
|
|
# Flow control for writes from APP socket |
|
|
|
cdef _control_app_writing(self, object context=None): |
|
cdef size_t size = self._get_write_buffer_size() |
|
if size >= self._outgoing_high_water and not self._app_writing_paused: |
|
self._app_writing_paused = True |
|
try: |
|
if context is None: |
|
# If the caller didn't provide a context, we assume the |
|
# caller is already in the right context, which is usually |
|
# inside the upstream callbacks like buffer_updated() |
|
self._app_protocol.pause_writing() |
|
else: |
|
run_in_context(context, 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: |
|
if context is None: |
|
# If the caller didn't provide a context, we assume the |
|
# caller is already in the right context, which is usually |
|
# inside the upstream callbacks like resume_writing() |
|
self._app_protocol.resume_writing() |
|
else: |
|
run_in_context(context, 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, |
|
}) |
|
|
|
cdef size_t _get_write_buffer_size(self): |
|
return self._outgoing.pending + self._write_buffer_size |
|
|
|
cdef _set_write_buffer_limits(self, high=None, low=None): |
|
high, low = add_flowcontrol_defaults( |
|
high, low, FLOW_CONTROL_HIGH_WATER_SSL_WRITE) |
|
self._outgoing_high_water = high |
|
self._outgoing_low_water = low |
|
|
|
# Flow control for reads to APP socket |
|
|
|
cdef _pause_reading(self): |
|
self._app_reading_paused = True |
|
|
|
cdef _resume_reading(self, object context): |
|
if self._app_reading_paused: |
|
self._app_reading_paused = False |
|
if self._state == WRAPPED: |
|
self._loop._call_soon_handle( |
|
new_MethodHandle(self._loop, |
|
"SSLProtocol._do_read", |
|
<method_t>self._do_read, |
|
context, |
|
self)) |
|
|
|
# Flow control for reads from SSL socket |
|
|
|
cdef _control_ssl_reading(self): |
|
cdef size_t 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() |
|
|
|
cdef _set_read_buffer_limits(self, high=None, low=None): |
|
high, low = add_flowcontrol_defaults( |
|
high, low, FLOW_CONTROL_HIGH_WATER_SSL_READ) |
|
self._incoming_high_water = high |
|
self._incoming_low_water = low |
|
|
|
cdef size_t _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 |
|
|
|
if self._state == WRAPPED: |
|
self._process_outgoing() |
|
self._control_app_writing() |
|
|
|
elif self._state == FLUSHING: |
|
self._do_flush() |
|
|
|
elif self._state == SHUTDOWN: |
|
self._do_shutdown() |
|
|
|
cdef _fatal_error(self, exc, message='Fatal error on transport'): |
|
if self._app_transport: |
|
self._app_transport._force_close(exc) |
|
elif self._transport: |
|
self._transport._force_close(exc) |
|
|
|
if isinstance(exc, OSError): |
|
if self._loop.get_debug(): |
|
aio_logger.debug("%r: %s", self, message, exc_info=True) |
|
elif not isinstance(exc, aio_CancelledError): |
|
self._loop.call_exception_handler({ |
|
'message': message, |
|
'exception': exc, |
|
'transport': self._transport, |
|
'protocol': self, |
|
})
|
|
|