Package cherrypy :: Package wsgiserver :: Module ssl_pyopenssl
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.wsgiserver.ssl_pyopenssl

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