OpenSecurity/install/web.py-0.37/web/wsgiserver/ssl_pyopenssl.py
author om
Mon, 02 Dec 2013 14:02:05 +0100
changeset 3 65432e6c6042
permissions -rwxr-xr-x
initial deployment and project layout commit
     1 """A library for integrating pyOpenSSL with CherryPy.
     2 
     3 The OpenSSL module must be importable for SSL functionality.
     4 You can obtain it from http://pyopenssl.sourceforge.net/
     5 
     6 To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
     7 SSLAdapter. There are two ways to use SSL:
     8 
     9 Method One
    10 ----------
    11 
    12  * ``ssl_adapter.context``: an instance of SSL.Context.
    13 
    14 If this is not None, it is assumed to be an SSL.Context instance,
    15 and will be passed to SSL.Connection on bind(). The developer is
    16 responsible for forming a valid Context object. This approach is
    17 to be preferred for more flexibility, e.g. if the cert and key are
    18 streams instead of files, or need decryption, or SSL.SSLv3_METHOD
    19 is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
    20 the pyOpenSSL documentation for complete options.
    21 
    22 Method Two (shortcut)
    23 ---------------------
    24 
    25  * ``ssl_adapter.certificate``: the filename of the server SSL certificate.
    26  * ``ssl_adapter.private_key``: the filename of the server's private key file.
    27 
    28 Both are None by default. If ssl_adapter.context is None, but .private_key
    29 and .certificate are both given and valid, they will be read, and the
    30 context will be automatically created from them.
    31 """
    32 
    33 import socket
    34 import threading
    35 import time
    36 
    37 from cherrypy import wsgiserver
    38 
    39 try:
    40     from OpenSSL import SSL
    41     from OpenSSL import crypto
    42 except ImportError:
    43     SSL = None
    44 
    45 
    46 class SSL_fileobject(wsgiserver.CP_fileobject):
    47     """SSL file object attached to a socket object."""
    48     
    49     ssl_timeout = 3
    50     ssl_retry = .01
    51     
    52     def _safe_call(self, is_reader, call, *args, **kwargs):
    53         """Wrap the given call with SSL error-trapping.
    54         
    55         is_reader: if False EOF errors will be raised. If True, EOF errors
    56         will return "" (to emulate normal sockets).
    57         """
    58         start = time.time()
    59         while True:
    60             try:
    61                 return call(*args, **kwargs)
    62             except SSL.WantReadError:
    63                 # Sleep and try again. This is dangerous, because it means
    64                 # the rest of the stack has no way of differentiating
    65                 # between a "new handshake" error and "client dropped".
    66                 # Note this isn't an endless loop: there's a timeout below.
    67                 time.sleep(self.ssl_retry)
    68             except SSL.WantWriteError:
    69                 time.sleep(self.ssl_retry)
    70             except SSL.SysCallError, e:
    71                 if is_reader and e.args == (-1, 'Unexpected EOF'):
    72                     return ""
    73                 
    74                 errnum = e.args[0]
    75                 if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
    76                     return ""
    77                 raise socket.error(errnum)
    78             except SSL.Error, e:
    79                 if is_reader and e.args == (-1, 'Unexpected EOF'):
    80                     return ""
    81                 
    82                 thirdarg = None
    83                 try:
    84                     thirdarg = e.args[0][0][2]
    85                 except IndexError:
    86                     pass
    87                 
    88                 if thirdarg == 'http request':
    89                     # The client is talking HTTP to an HTTPS server.
    90                     raise wsgiserver.NoSSLError()
    91                 
    92                 raise wsgiserver.FatalSSLAlert(*e.args)
    93             except:
    94                 raise
    95             
    96             if time.time() - start > self.ssl_timeout:
    97                 raise socket.timeout("timed out")
    98     
    99     def recv(self, *args, **kwargs):
   100         buf = []
   101         r = super(SSL_fileobject, self).recv
   102         while True:
   103             data = self._safe_call(True, r, *args, **kwargs)
   104             buf.append(data)
   105             p = self._sock.pending()
   106             if not p:
   107                 return "".join(buf)
   108     
   109     def sendall(self, *args, **kwargs):
   110         return self._safe_call(False, super(SSL_fileobject, self).sendall,
   111                                *args, **kwargs)
   112 
   113     def send(self, *args, **kwargs):
   114         return self._safe_call(False, super(SSL_fileobject, self).send,
   115                                *args, **kwargs)
   116 
   117 
   118 class SSLConnection:
   119     """A thread-safe wrapper for an SSL.Connection.
   120     
   121     ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
   122     """
   123     
   124     def __init__(self, *args):
   125         self._ssl_conn = SSL.Connection(*args)
   126         self._lock = threading.RLock()
   127     
   128     for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
   129               'renegotiate', 'bind', 'listen', 'connect', 'accept',
   130               'setblocking', 'fileno', 'close', 'get_cipher_list',
   131               'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
   132               'makefile', 'get_app_data', 'set_app_data', 'state_string',
   133               'sock_shutdown', 'get_peer_certificate', 'want_read',
   134               'want_write', 'set_connect_state', 'set_accept_state',
   135               'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
   136         exec("""def %s(self, *args):
   137         self._lock.acquire()
   138         try:
   139             return self._ssl_conn.%s(*args)
   140         finally:
   141             self._lock.release()
   142 """ % (f, f))
   143     
   144     def shutdown(self, *args):
   145         self._lock.acquire()
   146         try:
   147             # pyOpenSSL.socket.shutdown takes no args
   148             return self._ssl_conn.shutdown()
   149         finally:
   150             self._lock.release()
   151 
   152 
   153 class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
   154     """A wrapper for integrating pyOpenSSL with CherryPy."""
   155     
   156     context = None
   157     """An instance of SSL.Context."""
   158     
   159     certificate = None
   160     """The filename of the server SSL certificate."""
   161     
   162     private_key = None
   163     """The filename of the server's private key file."""
   164     
   165     certificate_chain = None
   166     """Optional. The filename of CA's intermediate certificate bundle.
   167     
   168     This is needed for cheaper "chained root" SSL certificates, and should be
   169     left as None if not required."""
   170     
   171     def __init__(self, certificate, private_key, certificate_chain=None):
   172         if SSL is None:
   173             raise ImportError("You must install pyOpenSSL to use HTTPS.")
   174         
   175         self.context = None
   176         self.certificate = certificate
   177         self.private_key = private_key
   178         self.certificate_chain = certificate_chain
   179         self._environ = None
   180     
   181     def bind(self, sock):
   182         """Wrap and return the given socket."""
   183         if self.context is None:
   184             self.context = self.get_context()
   185         conn = SSLConnection(self.context, sock)
   186         self._environ = self.get_environ()
   187         return conn
   188     
   189     def wrap(self, sock):
   190         """Wrap and return the given socket, plus WSGI environ entries."""
   191         return sock, self._environ.copy()
   192     
   193     def get_context(self):
   194         """Return an SSL.Context from self attributes."""
   195         # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
   196         c = SSL.Context(SSL.SSLv23_METHOD)
   197         c.use_privatekey_file(self.private_key)
   198         if self.certificate_chain:
   199             c.load_verify_locations(self.certificate_chain)
   200         c.use_certificate_file(self.certificate)
   201         return c
   202     
   203     def get_environ(self):
   204         """Return WSGI environ entries to be merged into each request."""
   205         ssl_environ = {
   206             "HTTPS": "on",
   207             # pyOpenSSL doesn't provide access to any of these AFAICT
   208 ##            'SSL_PROTOCOL': 'SSLv2',
   209 ##            SSL_CIPHER 	string 	The cipher specification name
   210 ##            SSL_VERSION_INTERFACE 	string 	The mod_ssl program version
   211 ##            SSL_VERSION_LIBRARY 	string 	The OpenSSL program version
   212             }
   213         
   214         if self.certificate:
   215             # Server certificate attributes
   216             cert = open(self.certificate, 'rb').read()
   217             cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
   218             ssl_environ.update({
   219                 'SSL_SERVER_M_VERSION': cert.get_version(),
   220                 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
   221 ##                'SSL_SERVER_V_START': Validity of server's certificate (start time),
   222 ##                'SSL_SERVER_V_END': Validity of server's certificate (end time),
   223                 })
   224             
   225             for prefix, dn in [("I", cert.get_issuer()),
   226                                ("S", cert.get_subject())]:
   227                 # X509Name objects don't seem to have a way to get the
   228                 # complete DN string. Use str() and slice it instead,
   229                 # because str(dn) == "<X509Name object '/C=US/ST=...'>"
   230                 dnstr = str(dn)[18:-2]
   231                 
   232                 wsgikey = 'SSL_SERVER_%s_DN' % prefix
   233                 ssl_environ[wsgikey] = dnstr
   234                 
   235                 # The DN should be of the form: /k1=v1/k2=v2, but we must allow
   236                 # for any value to contain slashes itself (in a URL).
   237                 while dnstr:
   238                     pos = dnstr.rfind("=")
   239                     dnstr, value = dnstr[:pos], dnstr[pos + 1:]
   240                     pos = dnstr.rfind("/")
   241                     dnstr, key = dnstr[:pos], dnstr[pos + 1:]
   242                     if key and value:
   243                         wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
   244                         ssl_environ[wsgikey] = value
   245         
   246         return ssl_environ
   247     
   248     def makefile(self, sock, mode='r', bufsize=-1):
   249         if SSL and isinstance(sock, SSL.ConnectionType):
   250             timeout = sock.gettimeout()
   251             f = SSL_fileobject(sock, mode, bufsize)
   252             f.ssl_timeout = timeout
   253             return f
   254         else:
   255             return wsgiserver.CP_fileobject(sock, mode, bufsize)
   256