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