On 2017-01-26 15:23, Nick Coghlan wrote:
On 26 January 2017 at 10:50, Cory Benfield <cory@lukasa.co.uk> wrote:
On 26 Jan 2017, at 07:49, Nick Coghlan <ncoghlan@gmail.com> wrote:
Option 4: tls.TLSError, tls.WantReadError, tls.WantWriteError are defined as inheriting from ssl.SSLError, ssl.SSLWantReadError, and ssl.SSLWantWriteError *if* the latter are defined
Option 5: as with Option 4, but the "ssl" module is also changed such that it *always* defines at least ssl.SSLError, ssl.SSLWantReadError, and ssl.SSLWantWriteError (and perhaps some of the other APIs that can be emulated atop the new tls abstraction), even if OpenSSL itself is unavailable
Here’s my problem with this:
try: socket.recv(8192) except tls.WantWriteError: socket.write(some_buffer)
This code does not work with the legacy ssl module, because isinstance(ssl.SSLWantWriteError, tls.WantWriteError) is false. This means that we need to write a shim over the legacy ssl module that wraps *all* API calls, catches all exceptions and then translates them into subclasses of the tls error classes. That seems entirely batty to me.
OK, so we have two competing problems here:
1. How do we write *new* API client code that is agnostic to whether or not the security implementation is traditional ssl or a new tls backend? 2. How does a library that exports the ssl exceptions migrate to using a tls backend instead, without having to catch and rewrap tls exceptions in the legacy ones?
Meeting both constraints at the same time would require exception *aliases* rather than one set of exceptions inheriting from the other, such that we get:
assert tls.TLSError is ssl.SSLError assert tls.WantWriteError is ssl.SSLWantWriteError assert tls.WantReadError is ssl.SSLWantReadError
In an ideal world, that could be handled just by having the ssl module import the new tls module and alias the exceptions accordingly. Talking to Christian about it, things might be a little messier in practice due to the way the _ssl/ssl C/Python split works, but there shouldn't be any insurmountable barriers to going down the exception aliasing path in 3.7+.
Backports to older versions would still need to contend with these being different exception objects, but one possible way of tackling that would be to inject a dummy ssl module into sys.modules before the regular one had a chance to be imported.
For technical reasons it is beneficial to define SSL exception in C code. I don't want to force people to load _ssl to avoid the overhead of OpenSSL loading and initialization. After some brain storming with Nick I came up with the idea to define the exceptions in _socket. The _ssl module imports a PyCapsule from _socket any way. I can just piggyback on the PyCapsule. For maximum compatibility with Python 2, the tls module should subclass from socket.error anyway, too. Python 2:
import ssl ssl.SSLError.__mro__ (<class 'ssl.SSLError'>, <class 'socket.error'>, <type 'exceptions.IOError'>, <type 'exceptions.EnvironmentError'>, <type 'exceptions.StandardError'>, <type 'exceptions.Exception'>, <type 'exceptions.BaseException'>, <type 'object'>)
Python 3:
import ssl, socket ssl.SSLError.__mro__ (<class 'ssl.SSLError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>) socket.error <class 'OSError'>
This import block should (hopefully) cover all bases: try: # CPython 3.7+ from _socket import SSLError as TLSError from _socket import SSLWantWriteError as WantWriteError from _socket import SSLWantReadError as WantReadError except ImportError: # CPython < 3.7 and other implementations try: from ssl import SSLError as TLSError from ssl import SSLWantWriteError as WantWriteError from ssl import SSLWantReadError as WantReadError except ImportError: # ssl module is not available # Python 2: socket.error is a subclass of IOError # Python 3: socket.error is just an alias of OSError import socket class TLSError(socket.error): pass class WantWriteError(TLSError): pass class WantReadError(TLSError): pass Christian