diff options
author | noptuno <repollo.marrero@gmail.com> | 2023-04-28 02:29:30 +0200 |
---|---|---|
committer | noptuno <repollo.marrero@gmail.com> | 2023-04-28 02:29:30 +0200 |
commit | 355dee533bb34a571b9367820a63cccb668cf866 (patch) | |
tree | 838af886b4fec07320aeb10f0d1e74ba79e79b5c /venv/lib/python3.9/site-packages/trio/_ssl.py | |
parent | added pyproject.toml file (diff) | |
download | gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.gz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.bz2 gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.lz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.xz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.zst gpt4free-355dee533bb34a571b9367820a63cccb668cf866.zip |
Diffstat (limited to 'venv/lib/python3.9/site-packages/trio/_ssl.py')
-rw-r--r-- | venv/lib/python3.9/site-packages/trio/_ssl.py | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/venv/lib/python3.9/site-packages/trio/_ssl.py b/venv/lib/python3.9/site-packages/trio/_ssl.py new file mode 100644 index 00000000..f9bb90af --- /dev/null +++ b/venv/lib/python3.9/site-packages/trio/_ssl.py @@ -0,0 +1,927 @@ +# General theory of operation: +# +# We implement an API that closely mirrors the stdlib ssl module's blocking +# API, and we do it using the stdlib ssl module's non-blocking in-memory API. +# The stdlib non-blocking in-memory API is barely documented, and acts as a +# thin wrapper around openssl, whose documentation also leaves something to be +# desired. So here's the main things you need to know to understand the code +# in this file: +# +# We use an ssl.SSLObject, which exposes the four main I/O operations: +# +# - do_handshake: performs the initial handshake. Must be called once at the +# beginning of each connection; is a no-op once it's completed once. +# +# - write: takes some unencrypted data and attempts to send it to the remote +# peer. + +# - read: attempts to decrypt and return some data from the remote peer. +# +# - unwrap: this is weirdly named; maybe it helps to realize that the thing it +# wraps is called SSL_shutdown. It sends a cryptographically signed message +# saying "I'm closing this connection now", and then waits to receive the +# same from the remote peer (unless we already received one, in which case +# it returns immediately). +# +# All of these operations read and write from some in-memory buffers called +# "BIOs", which are an opaque OpenSSL-specific object that's basically +# semantically equivalent to a Python bytearray. When they want to send some +# bytes to the remote peer, they append them to the outgoing BIO, and when +# they want to receive some bytes from the remote peer, they try to pull them +# out of the incoming BIO. "Sending" always succeeds, because the outgoing BIO +# can always be extended to hold more data. "Receiving" acts sort of like a +# non-blocking socket: it might manage to get some data immediately, or it +# might fail and need to be tried again later. We can also directly add or +# remove data from the BIOs whenever we want. +# +# Now the problem is that while these I/O operations are opaque atomic +# operations from the point of view of us calling them, under the hood they +# might require some arbitrary sequence of sends and receives from the remote +# peer. This is particularly true for do_handshake, which generally requires a +# few round trips, but it's also true for write and read, due to an evil thing +# called "renegotiation". +# +# Renegotiation is the process by which one of the peers might arbitrarily +# decide to redo the handshake at any time. Did I mention it's evil? It's +# pretty evil, and almost universally hated. The HTTP/2 spec forbids the use +# of TLS renegotiation for HTTP/2 connections. TLS 1.3 removes it from the +# protocol entirely. It's impossible to trigger a renegotiation if using +# Python's ssl module. OpenSSL's renegotiation support is pretty buggy [1]. +# Nonetheless, it does get used in real life, mostly in two cases: +# +# 1) Normally in TLS 1.2 and below, when the client side of a connection wants +# to present a certificate to prove their identity, that certificate gets sent +# in plaintext. This is bad, because it means that anyone eavesdropping can +# see who's connecting – it's like sending your username in plain text. Not as +# bad as sending your password in plain text, but still, pretty bad. However, +# renegotiations *are* encrypted. So as a workaround, it's not uncommon for +# systems that want to use client certificates to first do an anonymous +# handshake, and then to turn around and do a second handshake (= +# renegotiation) and this time ask for a client cert. Or sometimes this is +# done on a case-by-case basis, e.g. a web server might accept a connection, +# read the request, and then once it sees the page you're asking for it might +# stop and ask you for a certificate. +# +# 2) In principle the same TLS connection can be used for an arbitrarily long +# time, and might transmit arbitrarily large amounts of data. But this creates +# a cryptographic problem: an attacker who has access to arbitrarily large +# amounts of data that's all encrypted using the same key may eventually be +# able to use this to figure out the key. Is this a real practical problem? I +# have no idea, I'm not a cryptographer. In any case, some people worry that +# it's a problem, so their TLS libraries are designed to automatically trigger +# a renegotiation every once in a while on some sort of timer. +# +# The end result is that you might be going along, minding your own business, +# and then *bam*! a wild renegotiation appears! And you just have to cope. +# +# The reason that coping with renegotiations is difficult is that some +# unassuming "read" or "write" call might find itself unable to progress until +# it does a handshake, which remember is a process with multiple round +# trips. So read might have to send data, and write might have to receive +# data, and this might happen multiple times. And some of those attempts might +# fail because there isn't any data yet, and need to be retried. Managing all +# this is pretty complicated. +# +# Here's how openssl (and thus the stdlib ssl module) handle this. All of the +# I/O operations above follow the same rules. When you call one of them: +# +# - it might write some data to the outgoing BIO +# - it might read some data from the incoming BIO +# - it might raise SSLWantReadError if it can't complete without reading more +# data from the incoming BIO. This is important: the "read" in ReadError +# refers to reading from the *underlying* stream. +# - (and in principle it might raise SSLWantWriteError too, but that never +# happens when using memory BIOs, so never mind) +# +# If it doesn't raise an error, then the operation completed successfully +# (though we still need to take any outgoing data out of the memory buffer and +# put it onto the wire). If it *does* raise an error, then we need to retry +# *exactly that method call* later – in particular, if a 'write' failed, we +# need to try again later *with the same data*, because openssl might have +# already committed some of the initial parts of our data to its output even +# though it didn't tell us that, and has remembered that the next time we call +# write it needs to skip the first 1024 bytes or whatever it is. (Well, +# technically, we're actually allowed to call 'write' again with a data buffer +# which is the same as our old one PLUS some extra stuff added onto the end, +# but in Trio that never comes up so never mind.) +# +# There are some people online who claim that once you've gotten a Want*Error +# then the *very next call* you make to openssl *must* be the same as the +# previous one. I'm pretty sure those people are wrong. In particular, it's +# okay to call write, get a WantReadError, and then call read a few times; +# it's just that *the next time you call write*, it has to be with the same +# data. +# +# One final wrinkle: we want our SSLStream to support full-duplex operation, +# i.e. it should be possible for one task to be calling send_all while another +# task is calling receive_some. But renegotiation makes this a big hassle, because +# even if SSLStream's restricts themselves to one task calling send_all and one +# task calling receive_some, those two tasks might end up both wanting to call +# send_all, or both to call receive_some at the same time *on the underlying +# stream*. So we have to do some careful locking to hide this problem from our +# users. +# +# (Renegotiation is evil.) +# +# So our basic strategy is to define a single helper method called "_retry", +# which has generic logic for dealing with SSLWantReadError, pushing data from +# the outgoing BIO to the wire, reading data from the wire to the incoming +# BIO, retrying an I/O call until it works, and synchronizing with other tasks +# that might be calling _retry concurrently. Basically it takes an SSLObject +# non-blocking in-memory method and converts it into a Trio async blocking +# method. _retry is only about 30 lines of code, but all these cases +# multiplied by concurrent calls make it extremely tricky, so there are lots +# of comments down below on the details, and a really extensive test suite in +# test_ssl.py. And now you know *why* it's so tricky, and can probably +# understand how it works. +# +# [1] https://rt.openssl.org/Ticket/Display.html?id=3712 + +# XX how closely should we match the stdlib API? +# - maybe suppress_ragged_eofs=False is a better default? +# - maybe check crypto folks for advice? +# - this is also interesting: https://bugs.python.org/issue8108#msg102867 + +# Definitely keep an eye on Cory's TLS API ideas on security-sig etc. + +# XX document behavior on cancellation/error (i.e.: all is lost abandon +# stream) +# docs will need to make very clear that this is different from all the other +# cancellations in core Trio + +import operator as _operator +import ssl as _stdlib_ssl +from enum import Enum as _Enum + +import trio + +from .abc import Stream, Listener +from ._highlevel_generic import aclose_forcefully +from . import _sync +from ._util import ConflictDetector, Final + +################################################################ +# SSLStream +################################################################ + +# Ideally, when the user calls SSLStream.receive_some() with no argument, then +# we should do exactly one call to self.transport_stream.receive_some(), +# decrypt everything we got, and return it. Unfortunately, the way openssl's +# API works, we have to pick how much data we want to allow when we call +# read(), and then it (potentially) triggers a call to +# transport_stream.receive_some(). So at the time we pick the amount of data +# to decrypt, we don't know how much data we've read. As a simple heuristic, +# we record the max amount of data returned by previous calls to +# transport_stream.receive_some(), and we use that for future calls to read(). +# But what do we use for the very first call? That's what this constant sets. +# +# Note that the value passed to read() is a limit on the amount of +# *decrypted* data, but we can only see the size of the *encrypted* data +# returned by transport_stream.receive_some(). TLS adds a small amount of +# framing overhead, and TLS compression is rarely used these days because it's +# insecure. So the size of the encrypted data should be a slight over-estimate +# of the size of the decrypted data, which is exactly what we want. +# +# The specific value is not really based on anything; it might be worth tuning +# at some point. But, if you have an TCP connection with the typical 1500 byte +# MTU and an initial window of 10 (see RFC 6928), then the initial burst of +# data will be limited to ~15000 bytes (or a bit less due to IP-level framing +# overhead), so this is chosen to be larger than that. +STARTING_RECEIVE_SIZE = 16384 + + +def _is_eof(exc): + # There appears to be a bug on Python 3.10, where SSLErrors + # aren't properly translated into SSLEOFErrors. + # This stringly-typed error check is borrowed from the AnyIO + # project. + return isinstance(exc, _stdlib_ssl.SSLEOFError) or ( + hasattr(exc, "strerror") and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror + ) + + +class NeedHandshakeError(Exception): + """Some :class:`SSLStream` methods can't return any meaningful data until + after the handshake. If you call them before the handshake, they raise + this error. + + """ + + +class _Once: + def __init__(self, afn, *args): + self._afn = afn + self._args = args + self.started = False + self._done = _sync.Event() + + async def ensure(self, *, checkpoint): + if not self.started: + self.started = True + await self._afn(*self._args) + self._done.set() + elif not checkpoint and self._done.is_set(): + return + else: + await self._done.wait() + + @property + def done(self): + return self._done.is_set() + + +_State = _Enum("_State", ["OK", "BROKEN", "CLOSED"]) + + +class SSLStream(Stream, metaclass=Final): + r"""Encrypted communication using SSL/TLS. + + :class:`SSLStream` wraps an arbitrary :class:`~trio.abc.Stream`, and + allows you to perform encrypted communication over it using the usual + :class:`~trio.abc.Stream` interface. You pass regular data to + :meth:`send_all`, then it encrypts it and sends the encrypted data on the + underlying :class:`~trio.abc.Stream`; :meth:`receive_some` takes encrypted + data out of the underlying :class:`~trio.abc.Stream` and decrypts it + before returning it. + + You should read the standard library's :mod:`ssl` documentation carefully + before attempting to use this class, and probably other general + documentation on SSL/TLS as well. SSL/TLS is subtle and quick to + anger. Really. I'm not kidding. + + Args: + transport_stream (~trio.abc.Stream): The stream used to transport + encrypted data. Required. + + ssl_context (~ssl.SSLContext): The :class:`~ssl.SSLContext` used for + this connection. Required. Usually created by calling + :func:`ssl.create_default_context`. + + server_hostname (str or None): The name of the server being connected + to. Used for `SNI + <https://en.wikipedia.org/wiki/Server_Name_Indication>`__ and for + validating the server's certificate (if hostname checking is + enabled). This is effectively mandatory for clients, and actually + mandatory if ``ssl_context.check_hostname`` is ``True``. + + server_side (bool): Whether this stream is acting as a client or + server. Defaults to False, i.e. client mode. + + https_compatible (bool): There are two versions of SSL/TLS commonly + encountered in the wild: the standard version, and the version used + for HTTPS (HTTP-over-SSL/TLS). + + Standard-compliant SSL/TLS implementations always send a + cryptographically signed ``close_notify`` message before closing the + connection. This is important because if the underlying transport + were simply closed, then there wouldn't be any way for the other + side to know whether the connection was intentionally closed by the + peer that they negotiated a cryptographic connection to, or by some + `man-in-the-middle + <https://en.wikipedia.org/wiki/Man-in-the-middle_attack>`__ attacker + who can't manipulate the cryptographic stream, but can manipulate + the transport layer (a so-called "truncation attack"). + + However, this part of the standard is widely ignored by real-world + HTTPS implementations, which means that if you want to interoperate + with them, then you NEED to ignore it too. + + Fortunately this isn't as bad as it sounds, because the HTTP + protocol already includes its own equivalent of ``close_notify``, so + doing this again at the SSL/TLS level is redundant. But not all + protocols do! Therefore, by default Trio implements the safer + standard-compliant version (``https_compatible=False``). But if + you're speaking HTTPS or some other protocol where + ``close_notify``\s are commonly skipped, then you should set + ``https_compatible=True``; with this setting, Trio will neither + expect nor send ``close_notify`` messages. + + If you have code that was written to use :class:`ssl.SSLSocket` and + now you're porting it to Trio, then it may be useful to know that a + difference between :class:`SSLStream` and :class:`ssl.SSLSocket` is + that :class:`~ssl.SSLSocket` implements the + ``https_compatible=True`` behavior by default. + + Attributes: + transport_stream (trio.abc.Stream): The underlying transport stream + that was passed to ``__init__``. An example of when this would be + useful is if you're using :class:`SSLStream` over a + :class:`~trio.SocketStream` and want to call the + :class:`~trio.SocketStream`'s :meth:`~trio.SocketStream.setsockopt` + method. + + Internally, this class is implemented using an instance of + :class:`ssl.SSLObject`, and all of :class:`~ssl.SSLObject`'s methods and + attributes are re-exported as methods and attributes on this class. + However, there is one difference: :class:`~ssl.SSLObject` has several + methods that return information about the encrypted connection, like + :meth:`~ssl.SSLSocket.cipher` or + :meth:`~ssl.SSLSocket.selected_alpn_protocol`. If you call them before the + handshake, when they can't possibly return useful data, then + :class:`ssl.SSLObject` returns None, but :class:`trio.SSLStream` + raises :exc:`NeedHandshakeError`. + + This also means that if you register a SNI callback using + `~ssl.SSLContext.sni_callback`, then the first argument your callback + receives will be a :class:`ssl.SSLObject`. + + """ + + # Note: any new arguments here should likely also be added to + # SSLListener.__init__, and maybe the open_ssl_over_tcp_* helpers. + def __init__( + self, + transport_stream, + ssl_context, + *, + server_hostname=None, + server_side=False, + https_compatible=False, + ): + self.transport_stream = transport_stream + self._state = _State.OK + self._https_compatible = https_compatible + self._outgoing = _stdlib_ssl.MemoryBIO() + self._delayed_outgoing = None + self._incoming = _stdlib_ssl.MemoryBIO() + self._ssl_object = ssl_context.wrap_bio( + self._incoming, + self._outgoing, + server_side=server_side, + server_hostname=server_hostname, + ) + # Tracks whether we've already done the initial handshake + self._handshook = _Once(self._do_handshake) + + # These are used to synchronize access to self.transport_stream + self._inner_send_lock = _sync.StrictFIFOLock() + self._inner_recv_count = 0 + self._inner_recv_lock = _sync.Lock() + + # These are used to make sure that our caller doesn't attempt to make + # multiple concurrent calls to send_all/wait_send_all_might_not_block + # or to receive_some. + self._outer_send_conflict_detector = ConflictDetector( + "another task is currently sending data on this SSLStream" + ) + self._outer_recv_conflict_detector = ConflictDetector( + "another task is currently receiving data on this SSLStream" + ) + + self._estimated_receive_size = STARTING_RECEIVE_SIZE + + _forwarded = { + "context", + "server_side", + "server_hostname", + "session", + "session_reused", + "getpeercert", + "selected_npn_protocol", + "cipher", + "shared_ciphers", + "compression", + "pending", + "get_channel_binding", + "selected_alpn_protocol", + "version", + } + + _after_handshake = { + "session_reused", + "getpeercert", + "selected_npn_protocol", + "cipher", + "shared_ciphers", + "compression", + "get_channel_binding", + "selected_alpn_protocol", + "version", + } + + def __getattr__(self, name): + if name in self._forwarded: + if name in self._after_handshake and not self._handshook.done: + raise NeedHandshakeError( + "call do_handshake() before calling {!r}".format(name) + ) + + return getattr(self._ssl_object, name) + else: + raise AttributeError(name) + + def __setattr__(self, name, value): + if name in self._forwarded: + setattr(self._ssl_object, name, value) + else: + super().__setattr__(name, value) + + def __dir__(self): + return super().__dir__() + list(self._forwarded) + + def _check_status(self): + if self._state is _State.OK: + return + elif self._state is _State.BROKEN: + raise trio.BrokenResourceError + elif self._state is _State.CLOSED: + raise trio.ClosedResourceError + else: # pragma: no cover + assert False + + # This is probably the single trickiest function in Trio. It has lots of + # comments, though, just make sure to think carefully if you ever have to + # touch it. The big comment at the top of this file will help explain + # too. + async def _retry(self, fn, *args, ignore_want_read=False, is_handshake=False): + await trio.lowlevel.checkpoint_if_cancelled() + yielded = False + finished = False + while not finished: + # WARNING: this code needs to be very careful with when it + # calls 'await'! There might be multiple tasks calling this + # function at the same time trying to do different operations, + # so we need to be careful to: + # + # 1) interact with the SSLObject, then + # 2) await on exactly one thing that lets us make forward + # progress, then + # 3) loop or exit + # + # In particular we don't want to yield while interacting with + # the SSLObject (because it's shared state, so someone else + # might come in and mess with it while we're suspended), and + # we don't want to yield *before* starting the operation that + # will help us make progress, because then someone else might + # come in and leapfrog us. + + # Call the SSLObject method, and get its result. + # + # NB: despite what the docs say, SSLWantWriteError can't + # happen – "Writes to memory BIOs will always succeed if + # memory is available: that is their size can grow + # indefinitely." + # https://wiki.openssl.org/index.php/Manual:BIO_s_mem(3) + want_read = False + ret = None + try: + ret = fn(*args) + except _stdlib_ssl.SSLWantReadError: + want_read = True + except (_stdlib_ssl.SSLError, _stdlib_ssl.CertificateError) as exc: + self._state = _State.BROKEN + raise trio.BrokenResourceError from exc + else: + finished = True + if ignore_want_read: + want_read = False + finished = True + to_send = self._outgoing.read() + + # Some versions of SSL_do_handshake have a bug in how they handle + # the TLS 1.3 handshake on the server side: after the handshake + # finishes, they automatically send session tickets, even though + # the client may not be expecting data to arrive at this point and + # sending it could cause a deadlock or lost data. This applies at + # least to OpenSSL 1.1.1c and earlier, and the OpenSSL devs + # currently have no plans to fix it: + # + # https://github.com/openssl/openssl/issues/7948 + # https://github.com/openssl/openssl/issues/7967 + # + # The correct behavior is to wait to send session tickets on the + # first call to SSL_write. (This is what BoringSSL does.) So, we + # use a heuristic to detect when OpenSSL has tried to send session + # tickets, and we manually delay sending them until the + # appropriate moment. For more discussion see: + # + # https://github.com/python-trio/trio/issues/819#issuecomment-517529763 + if ( + is_handshake + and not want_read + and self._ssl_object.server_side + and self._ssl_object.version() == "TLSv1.3" + ): + assert self._delayed_outgoing is None + self._delayed_outgoing = to_send + to_send = b"" + + # Outputs from the above code block are: + # + # - to_send: bytestring; if non-empty then we need to send + # this data to make forward progress + # + # - want_read: True if we need to receive_some some data to make + # forward progress + # + # - finished: False means that we need to retry the call to + # fn(*args) again, after having pushed things forward. True + # means we still need to do whatever was said (in particular + # send any data in to_send), but once we do then we're + # done. + # + # - ret: the operation's return value. (Meaningless unless + # finished is True.) + # + # Invariant: want_read and finished can't both be True at the + # same time. + # + # Now we need to move things forward. There are two things we + # might have to do, and any given operation might require + # either, both, or neither to proceed: + # + # - send the data in to_send + # + # - receive_some some data and put it into the incoming BIO + # + # Our strategy is: if there's data to send, send it; + # *otherwise* if there's data to receive_some, receive_some it. + # + # If both need to happen, then we only send. Why? Well, we + # know that *right now* we have to both send and receive_some + # before the operation can complete. But as soon as we yield, + # that information becomes potentially stale – e.g. while + # we're sending, some other task might go and receive_some the + # data we need and put it into the incoming BIO. And if it + # does, then we *definitely don't* want to do a receive_some – + # there might not be any more data coming, and we'd deadlock! + # We could do something tricky to keep track of whether a + # receive_some happens while we're sending, but the case where + # we have to do both is very unusual (only during a + # renegotiation), so it's better to keep things simple. So we + # do just one potentially-blocking operation, then check again + # for fresh information. + # + # And we prioritize sending over receiving because, if there + # are multiple tasks that want to receive_some, then it + # doesn't matter what order they go in. But if there are + # multiple tasks that want to send, then they each have + # different data, and the data needs to get put onto the wire + # in the same order that it was retrieved from the outgoing + # BIO. So if we have data to send, that *needs* to be the + # *very* *next* *thing* we do, to make sure no-one else sneaks + # in before us. Or if we can't send immediately because + # someone else is, then we at least need to get in line + # immediately. + if to_send: + # NOTE: This relies on the lock being strict FIFO fair! + async with self._inner_send_lock: + yielded = True + try: + if self._delayed_outgoing is not None: + to_send = self._delayed_outgoing + to_send + self._delayed_outgoing = None + await self.transport_stream.send_all(to_send) + except: + # Some unknown amount of our data got sent, and we + # don't know how much. This stream is doomed. + self._state = _State.BROKEN + raise + elif want_read: + # It's possible that someone else is already blocked in + # transport_stream.receive_some. If so then we want to + # wait for them to finish, but we don't want to call + # transport_stream.receive_some again ourselves; we just + # want to loop around and check if their contribution + # helped anything. So we make a note of how many times + # some task has been through here before taking the lock, + # and if it's changed by the time we get the lock, then we + # skip calling transport_stream.receive_some and loop + # around immediately. + recv_count = self._inner_recv_count + async with self._inner_recv_lock: + yielded = True + if recv_count == self._inner_recv_count: + data = await self.transport_stream.receive_some() + if not data: + self._incoming.write_eof() + else: + self._estimated_receive_size = max( + self._estimated_receive_size, len(data) + ) + self._incoming.write(data) + self._inner_recv_count += 1 + if not yielded: + await trio.lowlevel.cancel_shielded_checkpoint() + return ret + + async def _do_handshake(self): + try: + await self._retry(self._ssl_object.do_handshake, is_handshake=True) + except: + self._state = _State.BROKEN + raise + + async def do_handshake(self): + """Ensure that the initial handshake has completed. + + The SSL protocol requires an initial handshake to exchange + certificates, select cryptographic keys, and so forth, before any + actual data can be sent or received. You don't have to call this + method; if you don't, then :class:`SSLStream` will automatically + perform the handshake as needed, the first time you try to send or + receive data. But if you want to trigger it manually – for example, + because you want to look at the peer's certificate before you start + talking to them – then you can call this method. + + If the initial handshake is already in progress in another task, this + waits for it to complete and then returns. + + If the initial handshake has already completed, this returns + immediately without doing anything (except executing a checkpoint). + + .. warning:: If this method is cancelled, then it may leave the + :class:`SSLStream` in an unusable state. If this happens then any + future attempt to use the object will raise + :exc:`trio.BrokenResourceError`. + + """ + self._check_status() + await self._handshook.ensure(checkpoint=True) + + # Most things work if we don't explicitly force do_handshake to be called + # before calling receive_some or send_all, because openssl will + # automatically perform the handshake on the first SSL_{read,write} + # call. BUT, allowing openssl to do this will disable Python's hostname + # checking!!! See: + # https://bugs.python.org/issue30141 + # So we *definitely* have to make sure that do_handshake is called + # before doing anything else. + async def receive_some(self, max_bytes=None): + """Read some data from the underlying transport, decrypt it, and + return it. + + See :meth:`trio.abc.ReceiveStream.receive_some` for details. + + .. warning:: If this method is cancelled while the initial handshake + or a renegotiation are in progress, then it may leave the + :class:`SSLStream` in an unusable state. If this happens then any + future attempt to use the object will raise + :exc:`trio.BrokenResourceError`. + + """ + with self._outer_recv_conflict_detector: + self._check_status() + try: + await self._handshook.ensure(checkpoint=False) + except trio.BrokenResourceError as exc: + # For some reason, EOF before handshake sometimes raises + # SSLSyscallError instead of SSLEOFError (e.g. on my linux + # laptop, but not on appveyor). Thanks openssl. + if self._https_compatible and ( + isinstance(exc.__cause__, _stdlib_ssl.SSLSyscallError) + or _is_eof(exc.__cause__) + ): + await trio.lowlevel.checkpoint() + return b"" + else: + raise + if max_bytes is None: + # If we somehow have more data already in our pending buffer + # than the estimate receive size, bump up our size a bit for + # this read only. + max_bytes = max(self._estimated_receive_size, self._incoming.pending) + else: + max_bytes = _operator.index(max_bytes) + if max_bytes < 1: + raise ValueError("max_bytes must be >= 1") + try: + return await self._retry(self._ssl_object.read, max_bytes) + except trio.BrokenResourceError as exc: + # This isn't quite equivalent to just returning b"" in the + # first place, because we still end up with self._state set to + # BROKEN. But that's actually fine, because after getting an + # EOF on TLS then the only thing you can do is close the + # stream, and closing doesn't care about the state. + + if self._https_compatible and _is_eof(exc.__cause__): + await trio.lowlevel.checkpoint() + return b"" + else: + raise + + async def send_all(self, data): + """Encrypt some data and then send it on the underlying transport. + + See :meth:`trio.abc.SendStream.send_all` for details. + + .. warning:: If this method is cancelled, then it may leave the + :class:`SSLStream` in an unusable state. If this happens then any + attempt to use the object will raise + :exc:`trio.BrokenResourceError`. + + """ + with self._outer_send_conflict_detector: + self._check_status() + await self._handshook.ensure(checkpoint=False) + # SSLObject interprets write(b"") as an EOF for some reason, which + # is not what we want. + if not data: + await trio.lowlevel.checkpoint() + return + await self._retry(self._ssl_object.write, data) + + async def unwrap(self): + """Cleanly close down the SSL/TLS encryption layer, allowing the + underlying stream to be used for unencrypted communication. + + You almost certainly don't need this. + + Returns: + A pair ``(transport_stream, trailing_bytes)``, where + ``transport_stream`` is the underlying transport stream, and + ``trailing_bytes`` is a byte string. Since :class:`SSLStream` + doesn't necessarily know where the end of the encrypted data will + be, it can happen that it accidentally reads too much from the + underlying stream. ``trailing_bytes`` contains this extra data; you + should process it as if it was returned from a call to + ``transport_stream.receive_some(...)``. + + """ + with self._outer_recv_conflict_detector, self._outer_send_conflict_detector: + self._check_status() + await self._handshook.ensure(checkpoint=False) + await self._retry(self._ssl_object.unwrap) + transport_stream = self.transport_stream + self.transport_stream = None + self._state = _State.CLOSED + return (transport_stream, self._incoming.read()) + + async def aclose(self): + """Gracefully shut down this connection, and close the underlying + transport. + + If ``https_compatible`` is False (the default), then this attempts to + first send a ``close_notify`` and then close the underlying stream by + calling its :meth:`~trio.abc.AsyncResource.aclose` method. + + If ``https_compatible`` is set to True, then this simply closes the + underlying stream and marks this stream as closed. + + """ + if self._state is _State.CLOSED: + await trio.lowlevel.checkpoint() + return + if self._state is _State.BROKEN or self._https_compatible: + self._state = _State.CLOSED + await self.transport_stream.aclose() + return + try: + # https_compatible=False, so we're in spec-compliant mode and have + # to send close_notify so that the other side gets a cryptographic + # assurance that we've called aclose. Of course, we can't do + # anything cryptographic until after we've completed the + # handshake: + await self._handshook.ensure(checkpoint=False) + # Then, we call SSL_shutdown *once*, because we want to send a + # close_notify but *not* wait for the other side to send back a + # response. In principle it would be more polite to wait for the + # other side to reply with their own close_notify. However, if + # they aren't paying attention (e.g., if they're just sending + # data and not receiving) then we will never notice our + # close_notify and we'll be waiting forever. Eventually we'll time + # out (hopefully), but it's still kind of nasty. And we can't + # require the other side to always be receiving, because (a) + # backpressure is kind of important, and (b) I bet there are + # broken TLS implementations out there that don't receive all the + # time. (Like e.g. anyone using Python ssl in synchronous mode.) + # + # The send-then-immediately-close behavior is explicitly allowed + # by the TLS specs, so we're ok on that. + # + # Subtlety: SSLObject.unwrap will immediately call it a second + # time, and the second time will raise SSLWantReadError because + # there hasn't been time for the other side to respond + # yet. (Unless they spontaneously sent a close_notify before we + # called this, and it's either already been processed or gets + # pulled out of the buffer by Python's second call.) So the way to + # do what we want is to ignore SSLWantReadError on this call. + # + # Also, because the other side might have already sent + # close_notify and closed their connection then it's possible that + # our attempt to send close_notify will raise + # BrokenResourceError. This is totally legal, and in fact can happen + # with two well-behaved Trio programs talking to each other, so we + # don't want to raise an error. So we suppress BrokenResourceError + # here. (This is safe, because literally the only thing this call + # to _retry will do is send the close_notify alert, so that's + # surely where the error comes from.) + # + # FYI in some cases this could also raise SSLSyscallError which I + # think is because SSL_shutdown is terrible. (Check out that note + # at the bottom of the man page saying that it sometimes gets + # raised spuriously.) I haven't seen this since we switched to + # immediately closing the socket, and I don't know exactly what + # conditions cause it and how to respond, so for now we're just + # letting that happen. But if you start seeing it, then hopefully + # this will give you a little head start on tracking it down, + # because whoa did this puzzle us at the 2017 PyCon sprints. + # + # Also, if someone else is blocked in send/receive, then we aren't + # going to be able to do a clean shutdown. If that happens, we'll + # just do an unclean shutdown. + try: + await self._retry(self._ssl_object.unwrap, ignore_want_read=True) + except (trio.BrokenResourceError, trio.BusyResourceError): + pass + except: + # Failure! Kill the stream and move on. + await aclose_forcefully(self.transport_stream) + raise + else: + # Success! Gracefully close the underlying stream. + await self.transport_stream.aclose() + finally: + self._state = _State.CLOSED + + async def wait_send_all_might_not_block(self): + """See :meth:`trio.abc.SendStream.wait_send_all_might_not_block`.""" + # This method's implementation is deceptively simple. + # + # First, we take the outer send lock, because of Trio's standard + # semantics that wait_send_all_might_not_block and send_all + # conflict. + with self._outer_send_conflict_detector: + self._check_status() + # Then we take the inner send lock. We know that no other tasks + # are calling self.send_all or self.wait_send_all_might_not_block, + # because we have the outer_send_lock. But! There might be another + # task calling self.receive_some -> transport_stream.send_all, in + # which case if we were to call + # transport_stream.wait_send_all_might_not_block directly we'd + # have two tasks doing write-related operations on + # transport_stream simultaneously, which is not allowed. We + # *don't* want to raise this conflict to our caller, because it's + # purely an internal affair – all they did was call + # wait_send_all_might_not_block and receive_some at the same time, + # which is totally valid. And waiting for the lock is OK, because + # a call to send_all certainly wouldn't complete while the other + # task holds the lock. + async with self._inner_send_lock: + # Now we have the lock, which creates another potential + # problem: what if a call to self.receive_some attempts to do + # transport_stream.send_all now? It'll have to wait for us to + # finish! But that's OK, because we release the lock as soon + # as the underlying stream becomes writable, and the + # self.receive_some call wasn't going to make any progress + # until then anyway. + # + # Of course, this does mean we might return *before* the + # stream is logically writable, because immediately after we + # return self.receive_some might write some data and make it + # non-writable again. But that's OK too, + # wait_send_all_might_not_block only guarantees that it + # doesn't return late. + await self.transport_stream.wait_send_all_might_not_block() + + +class SSLListener(Listener[SSLStream], metaclass=Final): + """A :class:`~trio.abc.Listener` for SSL/TLS-encrypted servers. + + :class:`SSLListener` wraps around another Listener, and converts + all incoming connections to encrypted connections by wrapping them + in a :class:`SSLStream`. + + Args: + transport_listener (~trio.abc.Listener): The listener whose incoming + connections will be wrapped in :class:`SSLStream`. + + ssl_context (~ssl.SSLContext): The :class:`~ssl.SSLContext` that will be + used for incoming connections. + + https_compatible (bool): Passed on to :class:`SSLStream`. + + Attributes: + transport_listener (trio.abc.Listener): The underlying listener that was + passed to ``__init__``. + + """ + + def __init__( + self, + transport_listener, + ssl_context, + *, + https_compatible=False, + ): + self.transport_listener = transport_listener + self._ssl_context = ssl_context + self._https_compatible = https_compatible + + async def accept(self): + """Accept the next connection and wrap it in an :class:`SSLStream`. + + See :meth:`trio.abc.Listener.accept` for details. + + """ + transport_stream = await self.transport_listener.accept() + return SSLStream( + transport_stream, + self._ssl_context, + server_side=True, + https_compatible=self._https_compatible, + ) + + async def aclose(self): + """Close the transport listener.""" + await self.transport_listener.aclose() |