""" A library for integrating pyOpenSSL with Cheroot. The OpenSSL module must be importable for SSL functionality. You can obtain it from `here `_. To use this module, set HTTPServer.ssl_adapter to an instance of ssl.Adapter. There are two ways to use SSL: Method One ---------- * ``ssl_adapter.context``: an instance of SSL.Context. If this is not None, it is assumed to be an SSL.Context instance, and will be passed to SSL.Connection on bind(). The developer is responsible for forming a valid Context object. This approach is to be preferred for more flexibility, e.g. if the cert and key are streams instead of files, or need decryption, or SSL.SSLv3_METHOD is desired instead of the default SSL.SSLv23_METHOD, etc. Consult the pyOpenSSL documentation for complete options. Method Two (shortcut) --------------------- * ``ssl_adapter.certificate``: the filename of the server SSL certificate. * ``ssl_adapter.private_key``: the filename of the server's private key file. Both are None by default. If ssl_adapter.context is None, but .private_key and .certificate are both given and valid, they will be read, and the context will be automatically created from them. """ from __future__ import absolute_import, division, print_function __metaclass__ = type import socket import threading import time try: from OpenSSL import SSL from OpenSSL import crypto except ImportError: SSL = None from . import Adapter from .. import errors, server as cheroot_server from ..makefile import MakeFile class SSL_fileobject(MakeFile): """SSL file object attached to a socket object.""" ssl_timeout = 3 ssl_retry = .01 def _safe_call(self, is_reader, call, *args, **kwargs): """Wrap the given call with SSL error-trapping. is_reader: if False EOF errors will be raised. If True, EOF errors will return "" (to emulate normal sockets). """ start = time.time() while True: try: return call(*args, **kwargs) except SSL.WantReadError: # Sleep and try again. This is dangerous, because it means # the rest of the stack has no way of differentiating # between a "new handshake" error and "client dropped". # Note this isn't an endless loop: there's a timeout below. time.sleep(self.ssl_retry) except SSL.WantWriteError: time.sleep(self.ssl_retry) except SSL.SysCallError as e: if is_reader and e.args == (-1, 'Unexpected EOF'): return '' errnum = e.args[0] if is_reader and errnum in errors.socket_errors_to_ignore: return '' raise socket.error(errnum) except SSL.Error as e: if is_reader and e.args == (-1, 'Unexpected EOF'): return '' thirdarg = None try: thirdarg = e.args[0][0][2] except IndexError: pass if thirdarg == 'http request': # The client is talking HTTP to an HTTPS server. raise errors.NoSSLError() raise errors.FatalSSLAlert(*e.args) if time.time() - start > self.ssl_timeout: raise socket.timeout('timed out') def recv(self, size): """Receive message of a size from the socket.""" return self._safe_call(True, super(SSL_fileobject, self).recv, size) def sendall(self, *args, **kwargs): """Send whole message to the socket.""" return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs) def send(self, *args, **kwargs): """Send some part of message to the socket.""" return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs) class SSLConnection: """A thread-safe wrapper for an SSL.Connection. ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. """ def __init__(self, *args): """Initialize SSLConnection instance.""" self._ssl_conn = SSL.Connection(*args) self._lock = threading.RLock() for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', 'renegotiate', 'bind', 'listen', 'connect', 'accept', 'setblocking', 'fileno', 'close', 'get_cipher_list', 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', 'makefile', 'get_app_data', 'set_app_data', 'state_string', 'sock_shutdown', 'get_peer_certificate', 'want_read', 'want_write', 'set_connect_state', 'set_accept_state', 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): exec("""def %s(self, *args): self._lock.acquire() try: return self._ssl_conn.%s(*args) finally: self._lock.release() """ % (f, f)) def shutdown(self, *args): """Shutdown the SSL connection. Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args. """ self._lock.acquire() try: return self._ssl_conn.shutdown() finally: self._lock.release() class pyOpenSSLAdapter(Adapter): """A wrapper for integrating pyOpenSSL with Cheroot.""" certificate = None """The filename of the server SSL certificate.""" private_key = None """The filename of the server's private key file.""" certificate_chain = None """Optional. The filename of CA's intermediate certificate bundle. This is needed for cheaper "chained root" SSL certificates, and should be left as None if not required.""" context = None """An instance of SSL.Context.""" ciphers = None """The ciphers list of SSL.""" def __init__( self, certificate, private_key, certificate_chain=None, ciphers=None): """Initialize OpenSSL Adapter instance.""" if SSL is None: raise ImportError('You must install pyOpenSSL to use HTTPS.') super(pyOpenSSLAdapter, self).__init__( certificate, private_key, certificate_chain, ciphers) self._environ = None def bind(self, sock): """Wrap and return the given socket.""" if self.context is None: self.context = self.get_context() conn = SSLConnection(self.context, sock) self._environ = self.get_environ() return conn def wrap(self, sock): """Wrap and return the given socket, plus WSGI environ entries.""" return sock, self._environ.copy() def get_context(self): """Return an SSL.Context from self attributes.""" # See https://code.activestate.com/recipes/442473/ c = SSL.Context(SSL.SSLv23_METHOD) c.use_privatekey_file(self.private_key) if self.certificate_chain: c.load_verify_locations(self.certificate_chain) c.use_certificate_file(self.certificate) return c def get_environ(self): """Return WSGI environ entries to be merged into each request.""" ssl_environ = { 'HTTPS': 'on', # pyOpenSSL doesn't provide access to any of these AFAICT # 'SSL_PROTOCOL': 'SSLv2', # SSL_CIPHER string The cipher specification name # SSL_VERSION_INTERFACE string The mod_ssl program version # SSL_VERSION_LIBRARY string The OpenSSL program version } if self.certificate: # Server certificate attributes cert = open(self.certificate, 'rb').read() cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) ssl_environ.update({ 'SSL_SERVER_M_VERSION': cert.get_version(), 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), # 'SSL_SERVER_V_START': # Validity of server's certificate (start time), # 'SSL_SERVER_V_END': # Validity of server's certificate (end time), }) for prefix, dn in [('I', cert.get_issuer()), ('S', cert.get_subject())]: # X509Name objects don't seem to have a way to get the # complete DN string. Use str() and slice it instead, # because str(dn) == "" dnstr = str(dn)[18:-2] wsgikey = 'SSL_SERVER_%s_DN' % prefix ssl_environ[wsgikey] = dnstr # The DN should be of the form: /k1=v1/k2=v2, but we must allow # for any value to contain slashes itself (in a URL). while dnstr: pos = dnstr.rfind('=') dnstr, value = dnstr[:pos], dnstr[pos + 1:] pos = dnstr.rfind('/') dnstr, key = dnstr[:pos], dnstr[pos + 1:] if key and value: wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) ssl_environ[wsgikey] = value return ssl_environ def makefile(self, sock, mode='r', bufsize=-1): """Return socket file object.""" if SSL and isinstance(sock, SSL.ConnectionType): timeout = sock.gettimeout() f = SSL_fileobject(sock, mode, bufsize) f.ssl_timeout = timeout return f else: return cheroot_server.CP_fileobject(sock, mode, bufsize)