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

Source Code for Module web.web.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 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      ssl_adapter.context: an instance of SSL.Context. 
 11       
 12      If this is not None, it is assumed to be an SSL.Context instance, 
 13      and will be passed to SSL.Connection on bind(). The developer is 
 14      responsible for forming a valid Context object. This approach is 
 15      to be preferred for more flexibility, e.g. if the cert and key are 
 16      streams instead of files, or need decryption, or SSL.SSLv3_METHOD 
 17      is desired instead of the default SSL.SSLv23_METHOD, etc. Consult 
 18      the pyOpenSSL documentation for complete options. 
 19   
 20  Method Two (shortcut): 
 21      ssl_adapter.certificate: the filename of the server SSL certificate. 
 22      ssl_adapter.private_key: the filename of the server's private key file. 
 23       
 24      Both are None by default. If ssl_adapter.context is None, but .private_key 
 25      and .certificate are both given and valid, they will be read, and the 
 26      context will be automatically created from them. 
 27       
 28      ssl_adapter.certificate_chain: (optional) the filename of CA's intermediate 
 29          certificate bundle. This is needed for cheaper "chained root" SSL 
 30          certificates, and should be left as None if not required. 
 31  """ 
 32   
 33  import socket 
 34  import threading 
 35  import time 
 36   
 37  from web 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 - def __init__(self, certificate, private_key, certificate_chain=None, client_CA=None, ):
157 if SSL is None: 158 raise ImportError("You must install pyOpenSSL to use HTTPS.") 159 160 self.context = None 161 self.certificate = certificate 162 self.private_key = private_key 163 self.certificate_chain = certificate_chain 164 self.client_CA = client_CA 165 self._environ = None
166
167 - def bind(self, sock):
168 """Wrap and return the given socket.""" 169 if self.context is None: 170 self.context = self.get_context() 171 conn = SSLConnection(self.context, sock) 172 self._environ = self.get_environ() 173 return conn
174
175 - def wrap(self, sock):
176 """Wrap and return the given socket, plus WSGI environ entries.""" 177 return sock, self._environ.copy()
178
179 - def get_context(self):
180 """Return an SSL.Context from self attributes.""" 181 # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 182 c = SSL.Context(SSL.SSLv23_METHOD) 183 c.use_privatekey_file(self.private_key) 184 if self.certificate_chain: 185 c.load_verify_locations(self.certificate_chain) 186 c.use_certificate_file(self.certificate) 187 188 if self.client_CA: 189 c.load_client_ca(self.client_CA) 190 191 c.set_verify_depth(2) 192 c.load_verify_locations( self.client_CA ) 193 194 def callback( conn, cert, errno, depth, retcode ): 195 return retcode
196 197 c.set_verify( SSL.VERIFY_FAIL_IF_NO_PEER_CERT | SSL.VERIFY_PEER, callback) 198 return c
199 200
201 - def get_environ(self):
202 """Return WSGI environ entries to be merged into each request.""" 203 ssl_environ = { 204 "HTTPS": "on", 205 # pyOpenSSL doesn't provide access to any of these AFAICT 206 ## 'SSL_PROTOCOL': 'SSLv2', 207 ## SSL_CIPHER string The cipher specification name 208 ## SSL_VERSION_INTERFACE string The mod_ssl program version 209 ## SSL_VERSION_LIBRARY string The OpenSSL program version 210 } 211 212 if self.certificate: 213 # Server certificate attributes 214 cert = open(self.certificate, 'rb').read() 215 cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) 216 ssl_environ.update({ 217 'SSL_SERVER_M_VERSION': cert.get_version(), 218 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), 219 ## 'SSL_SERVER_V_START': Validity of server's certificate (start time), 220 ## 'SSL_SERVER_V_END': 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