Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 2471

Show
Ignore:
Timestamp:
08/01/09 16:55:00
Author:
fumanchu
Message:

Factored SSL out to separate modules.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cpserver.py

    r2377 r2471  
    5858    ssl_certificate_chain = None 
    5959    ssl_private_key = None 
     60    ssl_module = 'pyopenssl' 
    6061    nodelay = True 
    6162     
  • trunk/cherrypy/_cpwsgi_server.py

    r2130 r2471  
    22the framework-agnostic wsgiserver package. 
    33""" 
     4import sys 
    45 
    56import cherrypy 
     
    6263        self.protocol = self.server_adapter.protocol_version 
    6364        self.nodelay = self.server_adapter.nodelay 
    64         self.ssl_context = self.server_adapter.ssl_context 
    65         self.ssl_certificate = self.server_adapter.ssl_certificate 
    66         self.ssl_certificate_chain = self.server_adapter.ssl_certificate_chain 
    67         self.ssl_private_key = self.server_adapter.ssl_private_key 
     65         
     66        if self.server_adapter.ssl_context: 
     67            adapter_class = self.get_ssl_adapter_class() 
     68            s.ssl_adapter = adapter_class(self.server_adapter.ssl_certificate, 
     69                                          self.server_adapter.ssl_private_key, 
     70                                          self.server_adapter.ssl_certificate_chain) 
     71            s.ssl_adapter.context = self.server_adapter.ssl_context 
     72        elif self.server_adapter.ssl_certificate: 
     73            adapter_class = self.get_ssl_adapter_class() 
     74            s.ssl_adapter = adapter_class(self.server_adapter.ssl_certificate, 
     75                                          self.server_adapter.ssl_private_key, 
     76                                          self.server_adapter.ssl_certificate_chain) 
     77     
     78    def get_ssl_adapter_class(self): 
     79        adname = (self.server_adapter.ssl_module or 'pyopenssl').lower() 
     80        adapter = ssl_adapters[adname] 
     81        if isinstance(adapter, basestring): 
     82            last_dot = adapter.rfind(".") 
     83            attr_name = adapter[last_dot + 1:] 
     84            mod_path = adapter[:last_dot] 
     85             
     86            try: 
     87                mod = sys.modules[mod_path] 
     88                if mod is None: 
     89                    raise KeyError() 
     90            except KeyError: 
     91                # The last [''] is important. 
     92                mod = __import__(mod_path, globals(), locals(), ['']) 
     93             
     94            # Let an AttributeError propagate outward. 
     95            try: 
     96                adapter = getattr(mod, attr_name) 
     97            except AttributeError: 
     98                raise AttributeError("'%s' object has no attribute '%s'" 
     99                                     % (mod_path, attr_name)) 
     100         
     101        return adapter 
    68102 
     103# These may either be wsgiserver.SSLAdapter subclasses or the string names 
     104# of such classes (in which case they will be lazily loaded). 
     105ssl_adapters = { 
     106    'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter', 
     107    'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter', 
     108    } 
     109 
  • trunk/cherrypy/test/test.py

    r2460 r2471  
    126126        if hasattr(engine, "console_control_handler"): 
    127127            engine.console_control_handler.subscribe() 
     128        #engine.subscribe('log', lambda msg, level: sys.stderr.write(msg + os.linesep)) 
    128129        engine.start() 
    129130         
  • trunk/cherrypy/wsgiserver/__init__.py

    r2469 r2471  
    2222    server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) 
    2323     
    24 Want SSL support? Just set these attributes: 
    25      
    26     server.ssl_certificate = <filename> 
    27     server.ssl_private_key = <filename> 
    28      
    29     if __name__ == '__main__': 
    30         try: 
    31             server.start() 
    32         except KeyboardInterrupt: 
    33             server.stop() 
     24Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance. 
    3425 
    3526This won't call the CherryPy engine (application side) at all, only the 
     
    9990import warnings 
    10091 
    101 try: 
    102     from OpenSSL import SSL 
    103     from OpenSSL import crypto 
    104 except ImportError: 
    105     SSL = None 
    106  
    10792import errno 
    10893 
     
    133118    ) 
    134119socket_errors_to_ignore.append("timed out") 
     120socket_errors_to_ignore.append("The read operation timed out") 
    135121 
    136122socket_errors_nonblocking = plat_specific_errors( 
     
    11061092                    buf_len += n 
    11071093                return "".join(buffers) 
    1108      
    1109  
    1110 class SSL_fileobject(CP_fileobject): 
    1111     """SSL file object attached to a socket object.""" 
    1112      
    1113     ssl_timeout = 3 
    1114     ssl_retry = .01 
    1115      
    1116     def _safe_call(self, is_reader, call, *args, **kwargs): 
    1117         """Wrap the given call with SSL error-trapping. 
    1118          
    1119         is_reader: if False EOF errors will be raised. If True, EOF errors 
    1120             will return "" (to emulate normal sockets). 
    1121         """ 
    1122         start = time.time() 
    1123         while True: 
    1124             try: 
    1125                 return call(*args, **kwargs) 
    1126             except SSL.WantReadError: 
    1127                 # Sleep and try again. This is dangerous, because it means 
    1128                 # the rest of the stack has no way of differentiating 
    1129                 # between a "new handshake" error and "client dropped". 
    1130                 # Note this isn't an endless loop: there's a timeout below. 
    1131                 time.sleep(self.ssl_retry) 
    1132             except SSL.WantWriteError: 
    1133                 time.sleep(self.ssl_retry) 
    1134             except SSL.SysCallError, e: 
    1135                 if is_reader and e.args == (-1, 'Unexpected EOF'): 
    1136                     return "" 
    1137                  
    1138                 errnum = e.args[0] 
    1139                 if is_reader and errnum in socket_errors_to_ignore: 
    1140                     return "" 
    1141                 raise socket.error(errnum) 
    1142             except SSL.Error, e: 
    1143                 if is_reader and e.args == (-1, 'Unexpected EOF'): 
    1144                     return "" 
    1145                  
    1146                 thirdarg = None 
    1147                 try: 
    1148                     thirdarg = e.args[0][0][2] 
    1149                 except IndexError: 
    1150                     pass 
    1151                  
    1152                 if thirdarg == 'http request': 
    1153                     # The client is talking HTTP to an HTTPS server. 
    1154                     raise NoSSLError() 
    1155                 raise FatalSSLAlert(*e.args) 
    1156             except: 
    1157                 raise 
    1158              
    1159             if time.time() - start > self.ssl_timeout: 
    1160                 raise socket.timeout("timed out") 
    1161  
    1162     def recv(self, *args, **kwargs): 
    1163         buf = [] 
    1164         r = super(SSL_fileobject, self).recv 
    1165         while True: 
    1166             data = self._safe_call(True, r, *args, **kwargs) 
    1167             buf.append(data) 
    1168             p = self._sock.pending() 
    1169             if not p: 
    1170                 return "".join(buf) 
    1171      
    1172     def sendall(self, *args, **kwargs): 
    1173         return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs) 
    1174  
    1175     def send(self, *args, **kwargs): 
    1176         return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs) 
    11771094 
    11781095 
     
    11981115               } 
    11991116     
    1200     def __init__(self, sock, wsgi_app, environ): 
     1117    def __init__(self, sock, wsgi_app, environ, makefile=CP_fileobject): 
    12011118        self.socket = sock 
    12021119        self.wsgi_app = wsgi_app 
     
    12061123        self.environ.update(environ) 
    12071124         
    1208         if SSL and isinstance(sock, SSL.ConnectionType): 
    1209             timeout = sock.gettimeout() 
    1210             self.rfile = SSL_fileobject(sock, "rb", self.rbufsize) 
    1211             self.rfile.ssl_timeout = timeout 
    1212             self.wfile = SSL_fileobject(sock, "wb", -1) 
    1213             self.wfile.ssl_timeout = timeout 
    1214         else: 
    1215             self.rfile = CP_fileobject(sock, "rb", self.rbufsize) 
    1216             self.wfile = CP_fileobject(sock, "wb", -1) 
     1125        self.rfile = makefile(sock, "rb", self.rbufsize) 
     1126        self.wfile = makefile(sock, "wb", -1) 
    12171127         
    12181128        # Wrap wsgi.input but not HTTPConnection.rfile itself. 
     
    12571167                    # has already started being written. 
    12581168                    if req and not req.sent_headers: 
    1259                         req.simple_response("408 Request Timeout") 
     1169                        try: 
     1170                            req.simple_response("408 Request Timeout") 
     1171                        except FatalSSLAlert: 
     1172                            # Close the connection. 
     1173                            return 
    12601174            elif errnum not in socket_errors_to_ignore: 
    12611175                if req and not req.sent_headers: 
    1262                     req.simple_response("500 Internal Server Error", 
    1263                                         format_exc()) 
     1176                    try: 
     1177                        req.simple_response("500 Internal Server Error", 
     1178                                            format_exc()) 
     1179                    except FatalSSLAlert: 
     1180                        # Close the connection. 
     1181                        return 
    12641182            return 
    12651183        except (KeyboardInterrupt, SystemExit): 
    12661184            raise 
    1267         except FatalSSLAlert, e
     1185        except FatalSSLAlert
    12681186            # Close the connection. 
    12691187            return 
     
    12761194                    "this server only speaks HTTPS on this port.") 
    12771195                self.linger = True 
    1278         except Exception, e
     1196        except Exception
    12791197            if req and not req.sent_headers: 
    1280                 req.simple_response("500 Internal Server Error", format_exc()) 
     1198                try: 
     1199                    req.simple_response("500 Internal Server Error", format_exc()) 
     1200                except FatalSSLAlert: 
     1201                    # Close the connection. 
     1202                    return 
    12811203     
    12821204    linger = False 
     
    12921214            # must be called *before* calling socket.close(), because the latter 
    12931215            # drops its reference to the kernel socket. 
    1294             self.socket._sock.close() 
     1216            if hasattr(self.socket, '_sock'): 
     1217                self.socket._sock.close() 
    12951218            self.socket.close() 
    12961219        else: 
     
    14381361                            c = worker.conn 
    14391362                            if c and not c.rfile.closed: 
    1440                                 if SSL and isinstance(c.socket, SSL.ConnectionType): 
    1441                                     # pyOpenSSL.socket.shutdown takes no args 
    1442                                     c.socket.shutdown() 
    1443                                 else: 
    1444                                     c.socket.shutdown(socket.SHUT_RD) 
     1363                                c.socket.shutdown(socket.SHUT_RD) 
    14451364                            worker.join() 
    14461365                except (AssertionError, 
     
    14501369                    pass 
    14511370 
    1452  
    1453  
    1454 class SSLConnection: 
    1455     """A thread-safe wrapper for an SSL.Connection. 
    1456      
    1457     *args: the arguments to create the wrapped SSL.Connection(*args). 
    1458     """ 
    1459      
    1460     def __init__(self, *args): 
    1461         self._ssl_conn = SSL.Connection(*args) 
    1462         self._lock = threading.RLock() 
    1463      
    1464     for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', 
    1465               'renegotiate', 'bind', 'listen', 'connect', 'accept', 
    1466               'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', 
    1467               'getpeername', 'getsockname', 'getsockopt', 'setsockopt', 
    1468               'makefile', 'get_app_data', 'set_app_data', 'state_string', 
    1469               'sock_shutdown', 'get_peer_certificate', 'want_read', 
    1470               'want_write', 'set_connect_state', 'set_accept_state', 
    1471               'connect_ex', 'sendall', 'settimeout'): 
    1472         exec("""def %s(self, *args): 
    1473         self._lock.acquire() 
    1474         try: 
    1475             return self._ssl_conn.%s(*args) 
    1476         finally: 
    1477             self._lock.release() 
    1478 """ % (f, f)) 
    14791371 
    14801372 
     
    15011393 
    15021394 
     1395class SSLAdapter(object): 
     1396     
     1397    def __init__(self, certificate, private_key, certificate_chain=None): 
     1398        self.certificate = certificate 
     1399        self.private_key = private_key 
     1400        self.certificate_chain = certificate_chain 
     1401     
     1402    def wrap(self, sock): 
     1403        raise NotImplemented 
     1404     
     1405    def makefile(self, sock, mode='r', bufsize=-1): 
     1406        raise NotImplemented 
     1407 
     1408 
    15031409class CherryPyWSGIServer(object): 
    15041410    """An HTTP server for WSGI. 
     
    15331439    SSL/HTTPS 
    15341440    --------- 
    1535     The OpenSSL module must be importable for SSL functionality. 
    1536     You can obtain it from http://pyopenssl.sourceforge.net/ 
    1537      
    1538     There are two ways to use SSL: 
    1539      
    1540     Method One: 
    1541         ssl_context: an instance of SSL.Context. 
    1542          
    1543         If this is not None, it is assumed to be an SSL.Context instance, 
    1544         and will be passed to SSL.Connection on bind(). The developer is 
    1545         responsible for forming a valid Context object. This approach is 
    1546         to be preferred for more flexibility, e.g. if the cert and key are 
    1547         streams instead of files, or need decryption, or SSL.SSLv3_METHOD 
    1548         is desired instead of the default SSL.SSLv23_METHOD, etc. Consult 
    1549         the pyOpenSSL documentation for complete options. 
    1550      
    1551     Method Two (shortcut): 
    1552         ssl_certificate: the filename of the server SSL certificate. 
    1553         ssl_privatekey: the filename of the server's private key file. 
    1554          
    1555         Both are None by default. If ssl_context is None, but ssl_privatekey 
    1556         and ssl_certificate are both given and valid, they will be read on 
    1557         server start, and self.ssl_context will be automatically created 
    1558         from them. 
    1559          
    1560         ssl_certificate_chain: (optional) the filename of CA's intermediate 
    1561             certificate bundle. This is needed for cheaper "chained root" SSL 
    1562             certificates, and should be left as None if not required. 
     1441    You must have an ssl library installed and set self.ssl_adapter to an 
     1442    instance of SSLAdapter (or a subclass) which provides the methods: 
     1443        wrap(sock) -> wrapped socket, ssl environ dict 
     1444        makefile(sock, mode='r', bufsize=-1) -> socket file object 
    15631445    """ 
    15641446     
     
    15741456    environ = {} 
    15751457     
    1576     # An SSL.Context instance... 
    1577     ssl_context = None 
    1578      
    1579     # ...or paths to certificate and private key files 
    1580     ssl_certificate = None 
    1581     ssl_certificate_chain = None 
    1582     ssl_private_key = None 
     1458    ssl_adapter = None 
    15831459     
    15841460    def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, 
     
    17101586            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 
    17111587         
    1712         if (self.ssl_context is None and 
    1713             self.ssl_certificate and self.ssl_private_key): 
    1714             if SSL is None: 
    1715                 raise ImportError("You must install pyOpenSSL to use HTTPS.") 
    1716              
    1717             # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 
    1718             self.ssl_context = SSL.Context(SSL.SSLv23_METHOD) 
    1719             self.ssl_context.use_privatekey_file(self.ssl_private_key) 
    1720             if self.ssl_certificate_chain: 
    1721                 self.ssl_context.load_verify_locations(self.ssl_certificate_chain) 
    1722             self.ssl_context.use_certificate_file(self.ssl_certificate) 
    1723          
    1724         if self.ssl_context is not None: 
    1725             self.socket = SSLConnection(self.ssl_context, self.socket) 
    1726             self.populate_ssl_environ() 
     1588        if self.ssl_adapter is not None: 
     1589            self.socket = self.ssl_adapter.bind(self.socket) 
    17271590         
    17281591        # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), 
     
    17431606        try: 
    17441607            s, addr = self.socket.accept() 
    1745             if addr is None: # sometimes this can happen 
    1746                 # figure out if AF_INET or AF_INET6. 
    1747                 if len(s.getsockname()) == 2: 
    1748                     # AF_INET 
    1749                     addr = ('0.0.0.0', 0) 
    1750                 else: 
    1751                     # AF_INET6 
    1752                     addr = ('::', 0) 
    1753             prevent_socket_inheritance(s) 
    17541608            if not self.ready: 
    17551609                return 
     1610             
     1611            prevent_socket_inheritance(s) 
    17561612            if hasattr(s, 'settimeout'): 
    17571613                s.settimeout(self.timeout) 
     
    17761632                # optional values 
    17771633                # Until we do DNS lookups, omit REMOTE_HOST 
     1634                if addr is None: # sometimes this can happen 
     1635                    # figure out if AF_INET or AF_INET6. 
     1636                    if len(s.getsockname()) == 2: 
     1637                        # AF_INET 
     1638                        addr = ('0.0.0.0', 0) 
     1639                    else: 
     1640                        # AF_INET6 
     1641                        addr = ('::', 0) 
    17781642                environ["REMOTE_ADDR"] = addr[0] 
    17791643                environ["REMOTE_PORT"] = str(addr[1]) 
    17801644             
    1781             conn = self.ConnectionClass(s, self.wsgi_app, environ) 
     1645            makefile = CP_fileobject 
     1646            # if ssl cert and key are set, we try to be a secure HTTP server 
     1647            if self.ssl_adapter is not None: 
     1648                s, ssl_env = self.ssl_adapter.wrap(s) 
     1649                if not s: 
     1650                    return 
     1651                environ.update(ssl_env) 
     1652                makefile = self.ssl_adapter.makefile 
     1653             
     1654            conn = self.ConnectionClass(s, self.wsgi_app, environ, makefile) 
    17821655            self.requests.put(conn) 
    17831656        except socket.timeout: 
     
    18521725         
    18531726        self.requests.stop(self.shutdown_timeout) 
    1854      
    1855     def populate_ssl_environ(self): 
    1856         """Create WSGI environ entries to be merged into each request.""" 
    1857         ssl_environ = { 
    1858             "wsgi.url_scheme": "https", 
    1859             "HTTPS": "on", 
    1860             # pyOpenSSL doesn't provide access to any of these AFAICT 
    1861 ##            'SSL_PROTOCOL': 'SSLv2', 
    1862 ##            SSL_CIPHER        string  The cipher specification name 
    1863 ##            SSL_VERSION_INTERFACE     string  The mod_ssl program version 
    1864 ##            SSL_VERSION_LIBRARY       string  The OpenSSL program version 
    1865             } 
    1866          
    1867         if self.ssl_certificate: 
    1868             # Server certificate attributes 
    1869             cert = open(self.ssl_certificate, 'rb').read() 
    1870             cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) 
    1871             ssl_environ.update({ 
    1872                 'SSL_SERVER_M_VERSION': cert.get_version(), 
    1873                 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), 
    1874 ##                'SSL_SERVER_V_START': Validity of server's certificate (start time), 
    1875 ##                'SSL_SERVER_V_END': Validity of server's certificate (end time), 
    1876                 }) 
    1877              
    1878             for prefix, dn in [("I", cert.get_issuer()), 
    1879                                ("S", cert.get_subject())]: 
    1880                 # X509Name objects don't seem to have a way to get the 
    1881                 # complete DN string. Use str() and slice it instead, 
    1882                 # because str(dn) == "<X509Name object '/C=US/ST=...'>" 
    1883                 dnstr = str(dn)[18:-2] 
    1884                  
    1885                 wsgikey = 'SSL_SERVER_%s_DN' % prefix 
    1886                 ssl_environ[wsgikey] = dnstr 
    1887                  
    1888                 # The DN should be of the form: /k1=v1/k2=v2, but we must allow 
    1889                 # for any value to contain slashes itself (in a URL). 
    1890                 while dnstr: 
    1891                     pos = dnstr.rfind("=") 
    1892                     dnstr, value = dnstr[:pos], dnstr[pos + 1:] 
    1893                     pos = dnstr.rfind("/") 
    1894                     dnstr, key = dnstr[:pos], dnstr[pos + 1:] 
    1895                     if key and value: 
    1896                         wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) 
    1897                         ssl_environ[wsgikey] = value 
    1898          
    1899         self.environ.update(ssl_environ) 
    1900  
     1727 

Hosted by WebFaction

Log in as guest/cpguest to create tickets