OLD | NEW |
(Empty) | |
| 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 |
OLD | NEW |