Changeset 2471
- Timestamp:
- 08/01/09 16:55:00
- Files:
-
- trunk/cherrypy/_cpserver.py (modified) (1 diff)
- trunk/cherrypy/_cpwsgi_server.py (modified) (2 diffs)
- trunk/cherrypy/test/test.py (modified) (1 diff)
- trunk/cherrypy/wsgiserver/__init__.py (modified) (18 diffs)
- trunk/cherrypy/wsgiserver/ssl_builtin.py (added)
- trunk/cherrypy/wsgiserver/ssl_pyopenssl.py (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/cherrypy/_cpserver.py
r2377 r2471 58 58 ssl_certificate_chain = None 59 59 ssl_private_key = None 60 ssl_module = 'pyopenssl' 60 61 nodelay = True 61 62 trunk/cherrypy/_cpwsgi_server.py
r2130 r2471 2 2 the framework-agnostic wsgiserver package. 3 3 """ 4 import sys 4 5 5 6 import cherrypy … … 62 63 self.protocol = self.server_adapter.protocol_version 63 64 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 68 102 103 # These may either be wsgiserver.SSLAdapter subclasses or the string names 104 # of such classes (in which case they will be lazily loaded). 105 ssl_adapters = { 106 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter', 107 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter', 108 } 109 trunk/cherrypy/test/test.py
r2460 r2471 126 126 if hasattr(engine, "console_control_handler"): 127 127 engine.console_control_handler.subscribe() 128 #engine.subscribe('log', lambda msg, level: sys.stderr.write(msg + os.linesep)) 128 129 engine.start() 129 130 trunk/cherrypy/wsgiserver/__init__.py
r2469 r2471 22 22 server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) 23 23 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() 24 Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance. 34 25 35 26 This won't call the CherryPy engine (application side) at all, only the … … 99 90 import warnings 100 91 101 try:102 from OpenSSL import SSL103 from OpenSSL import crypto104 except ImportError:105 SSL = None106 107 92 import errno 108 93 … … 133 118 ) 134 119 socket_errors_to_ignore.append("timed out") 120 socket_errors_to_ignore.append("The read operation timed out") 135 121 136 122 socket_errors_nonblocking = plat_specific_errors( … … 1106 1092 buf_len += n 1107 1093 return "".join(buffers) 1108 1109 1110 class SSL_fileobject(CP_fileobject):1111 """SSL file object attached to a socket object."""1112 1113 ssl_timeout = 31114 ssl_retry = .011115 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 errors1120 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 means1128 # the rest of the stack has no way of differentiating1129 # 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 = None1147 try:1148 thirdarg = e.args[0][0][2]1149 except IndexError:1150 pass1151 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 raise1158 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).recv1165 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)1177 1094 1178 1095 … … 1198 1115 } 1199 1116 1200 def __init__(self, sock, wsgi_app, environ ):1117 def __init__(self, sock, wsgi_app, environ, makefile=CP_fileobject): 1201 1118 self.socket = sock 1202 1119 self.wsgi_app = wsgi_app … … 1206 1123 self.environ.update(environ) 1207 1124 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) 1217 1127 1218 1128 # Wrap wsgi.input but not HTTPConnection.rfile itself. … … 1257 1167 # has already started being written. 1258 1168 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 1260 1174 elif errnum not in socket_errors_to_ignore: 1261 1175 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 1264 1182 return 1265 1183 except (KeyboardInterrupt, SystemExit): 1266 1184 raise 1267 except FatalSSLAlert , e:1185 except FatalSSLAlert: 1268 1186 # Close the connection. 1269 1187 return … … 1276 1194 "this server only speaks HTTPS on this port.") 1277 1195 self.linger = True 1278 except Exception , e:1196 except Exception: 1279 1197 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 1281 1203 1282 1204 linger = False … … 1292 1214 # must be called *before* calling socket.close(), because the latter 1293 1215 # drops its reference to the kernel socket. 1294 self.socket._sock.close() 1216 if hasattr(self.socket, '_sock'): 1217 self.socket._sock.close() 1295 1218 self.socket.close() 1296 1219 else: … … 1438 1361 c = worker.conn 1439 1362 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) 1445 1364 worker.join() 1446 1365 except (AssertionError, … … 1450 1369 pass 1451 1370 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))1479 1371 1480 1372 … … 1501 1393 1502 1394 1395 class 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 1503 1409 class CherryPyWSGIServer(object): 1504 1410 """An HTTP server for WSGI. … … 1533 1439 SSL/HTTPS 1534 1440 --------- 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 1563 1445 """ 1564 1446 … … 1574 1456 environ = {} 1575 1457 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 1583 1459 1584 1460 def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, … … 1710 1586 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 1711 1587 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) 1727 1590 1728 1591 # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), … … 1743 1606 try: 1744 1607 s, addr = self.socket.accept() 1745 if addr is None: # sometimes this can happen1746 # figure out if AF_INET or AF_INET6.1747 if len(s.getsockname()) == 2:1748 # AF_INET1749 addr = ('0.0.0.0', 0)1750 else:1751 # AF_INET61752 addr = ('::', 0)1753 prevent_socket_inheritance(s)1754 1608 if not self.ready: 1755 1609 return 1610 1611 prevent_socket_inheritance(s) 1756 1612 if hasattr(s, 'settimeout'): 1757 1613 s.settimeout(self.timeout) … … 1776 1632 # optional values 1777 1633 # 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) 1778 1642 environ["REMOTE_ADDR"] = addr[0] 1779 1643 environ["REMOTE_PORT"] = str(addr[1]) 1780 1644 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) 1782 1655 self.requests.put(conn) 1783 1656 except socket.timeout: … … 1852 1725 1853 1726 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

