You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
9.0 KiB
254 lines
9.0 KiB
5 years ago
|
"""A library for integrating pyOpenSSL with CherryPy.
|
||
|
|
||
|
The OpenSSL module must be importable for SSL functionality.
|
||
|
You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
|
||
|
|
||
|
To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
|
||
|
SSLAdapter. There are two ways to use SSL:
|
||
|
|
||
|
Method One
|
||
|
----------
|
||
|
|
||
|
* ``ssl_adapter.context``: an instance of SSL.Context.
|
||
|
|
||
|
If this is not None, it is assumed to be an SSL.Context instance,
|
||
|
and will be passed to SSL.Connection on bind(). The developer is
|
||
|
responsible for forming a valid Context object. This approach is
|
||
|
to be preferred for more flexibility, e.g. if the cert and key are
|
||
|
streams instead of files, or need decryption, or SSL.SSLv3_METHOD
|
||
|
is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
|
||
|
the pyOpenSSL documentation for complete options.
|
||
|
|
||
|
Method Two (shortcut)
|
||
|
---------------------
|
||
|
|
||
|
* ``ssl_adapter.certificate``: the filename of the server SSL certificate.
|
||
|
* ``ssl_adapter.private_key``: the filename of the server's private key file.
|
||
|
|
||
|
Both are None by default. If ssl_adapter.context is None, but .private_key
|
||
|
and .certificate are both given and valid, they will be read, and the
|
||
|
context will be automatically created from them.
|
||
|
"""
|
||
|
|
||
|
import socket
|
||
|
import threading
|
||
|
import time
|
||
|
|
||
|
from cherrypy import wsgiserver
|
||
|
|
||
|
try:
|
||
|
from OpenSSL import SSL
|
||
|
from OpenSSL import crypto
|
||
|
except ImportError:
|
||
|
SSL = None
|
||
|
|
||
|
|
||
|
class SSL_fileobject(wsgiserver.CP_makefile):
|
||
|
|
||
|
"""SSL file object attached to a socket object."""
|
||
|
|
||
|
ssl_timeout = 3
|
||
|
ssl_retry = .01
|
||
|
|
||
|
def _safe_call(self, is_reader, call, *args, **kwargs):
|
||
|
"""Wrap the given call with SSL error-trapping.
|
||
|
|
||
|
is_reader: if False EOF errors will be raised. If True, EOF errors
|
||
|
will return "" (to emulate normal sockets).
|
||
|
"""
|
||
|
start = time.time()
|
||
|
while True:
|
||
|
try:
|
||
|
return call(*args, **kwargs)
|
||
|
except SSL.WantReadError:
|
||
|
# Sleep and try again. This is dangerous, because it means
|
||
|
# the rest of the stack has no way of differentiating
|
||
|
# between a "new handshake" error and "client dropped".
|
||
|
# Note this isn't an endless loop: there's a timeout below.
|
||
|
time.sleep(self.ssl_retry)
|
||
|
except SSL.WantWriteError:
|
||
|
time.sleep(self.ssl_retry)
|
||
|
except SSL.SysCallError as e:
|
||
|
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||
|
return ''
|
||
|
|
||
|
errnum = e.args[0]
|
||
|
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
|
||
|
return ''
|
||
|
raise socket.error(errnum)
|
||
|
except SSL.Error as e:
|
||
|
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||
|
return ''
|
||
|
|
||
|
thirdarg = None
|
||
|
try:
|
||
|
thirdarg = e.args[0][0][2]
|
||
|
except IndexError:
|
||
|
pass
|
||
|
|
||
|
if thirdarg == 'http request':
|
||
|
# The client is talking HTTP to an HTTPS server.
|
||
|
raise wsgiserver.NoSSLError()
|
||
|
|
||
|
raise wsgiserver.FatalSSLAlert(*e.args)
|
||
|
except:
|
||
|
raise
|
||
|
|
||
|
if time.time() - start > self.ssl_timeout:
|
||
|
raise socket.timeout('timed out')
|
||
|
|
||
|
def recv(self, size):
|
||
|
return self._safe_call(True, super(SSL_fileobject, self).recv, size)
|
||
|
|
||
|
def sendall(self, *args, **kwargs):
|
||
|
return self._safe_call(False, super(SSL_fileobject, self).sendall,
|
||
|
*args, **kwargs)
|
||
|
|
||
|
def send(self, *args, **kwargs):
|
||
|
return self._safe_call(False, super(SSL_fileobject, self).send,
|
||
|
*args, **kwargs)
|
||
|
|
||
|
|
||
|
class SSLConnection:
|
||
|
|
||
|
"""A thread-safe wrapper for an SSL.Connection.
|
||
|
|
||
|
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, *args):
|
||
|
self._ssl_conn = SSL.Connection(*args)
|
||
|
self._lock = threading.RLock()
|
||
|
|
||
|
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
|
||
|
'renegotiate', 'bind', 'listen', 'connect', 'accept',
|
||
|
'setblocking', 'fileno', 'close', 'get_cipher_list',
|
||
|
'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
|
||
|
'makefile', 'get_app_data', 'set_app_data', 'state_string',
|
||
|
'sock_shutdown', 'get_peer_certificate', 'want_read',
|
||
|
'want_write', 'set_connect_state', 'set_accept_state',
|
||
|
'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
|
||
|
exec("""def %s(self, *args):
|
||
|
self._lock.acquire()
|
||
|
try:
|
||
|
return self._ssl_conn.%s(*args)
|
||
|
finally:
|
||
|
self._lock.release()
|
||
|
""" % (f, f))
|
||
|
|
||
|
def shutdown(self, *args):
|
||
|
self._lock.acquire()
|
||
|
try:
|
||
|
# pyOpenSSL.socket.shutdown takes no args
|
||
|
return self._ssl_conn.shutdown()
|
||
|
finally:
|
||
|
self._lock.release()
|
||
|
|
||
|
|
||
|
class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||
|
|
||
|
"""A wrapper for integrating pyOpenSSL with CherryPy."""
|
||
|
|
||
|
context = None
|
||
|
"""An instance of SSL.Context."""
|
||
|
|
||
|
certificate = None
|
||
|
"""The filename of the server SSL certificate."""
|
||
|
|
||
|
private_key = None
|
||
|
"""The filename of the server's private key file."""
|
||
|
|
||
|
certificate_chain = None
|
||
|
"""Optional. The filename of CA's intermediate certificate bundle.
|
||
|
|
||
|
This is needed for cheaper "chained root" SSL certificates, and should be
|
||
|
left as None if not required."""
|
||
|
|
||
|
def __init__(self, certificate, private_key, certificate_chain=None):
|
||
|
if SSL is None:
|
||
|
raise ImportError('You must install pyOpenSSL to use HTTPS.')
|
||
|
|
||
|
self.context = None
|
||
|
self.certificate = certificate
|
||
|
self.private_key = private_key
|
||
|
self.certificate_chain = certificate_chain
|
||
|
self._environ = None
|
||
|
|
||
|
def bind(self, sock):
|
||
|
"""Wrap and return the given socket."""
|
||
|
if self.context is None:
|
||
|
self.context = self.get_context()
|
||
|
conn = SSLConnection(self.context, sock)
|
||
|
self._environ = self.get_environ()
|
||
|
return conn
|
||
|
|
||
|
def wrap(self, sock):
|
||
|
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||
|
return sock, self._environ.copy()
|
||
|
|
||
|
def get_context(self):
|
||
|
"""Return an SSL.Context from self attributes."""
|
||
|
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
|
||
|
c = SSL.Context(SSL.SSLv23_METHOD)
|
||
|
c.use_privatekey_file(self.private_key)
|
||
|
if self.certificate_chain:
|
||
|
c.load_verify_locations(self.certificate_chain)
|
||
|
c.use_certificate_file(self.certificate)
|
||
|
return c
|
||
|
|
||
|
def get_environ(self):
|
||
|
"""Return WSGI environ entries to be merged into each request."""
|
||
|
ssl_environ = {
|
||
|
'HTTPS': 'on',
|
||
|
# pyOpenSSL doesn't provide access to any of these AFAICT
|
||
|
# 'SSL_PROTOCOL': 'SSLv2',
|
||
|
# SSL_CIPHER string The cipher specification name
|
||
|
# SSL_VERSION_INTERFACE string The mod_ssl program version
|
||
|
# SSL_VERSION_LIBRARY string The OpenSSL program version
|
||
|
}
|
||
|
|
||
|
if self.certificate:
|
||
|
# Server certificate attributes
|
||
|
cert = open(self.certificate, 'rb').read()
|
||
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
||
|
ssl_environ.update({
|
||
|
'SSL_SERVER_M_VERSION': cert.get_version(),
|
||
|
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
|
||
|
# 'SSL_SERVER_V_START':
|
||
|
# Validity of server's certificate (start time),
|
||
|
# 'SSL_SERVER_V_END':
|
||
|
# Validity of server's certificate (end time),
|
||
|
})
|
||
|
|
||
|
for prefix, dn in [('I', cert.get_issuer()),
|
||
|
('S', cert.get_subject())]:
|
||
|
# X509Name objects don't seem to have a way to get the
|
||
|
# complete DN string. Use str() and slice it instead,
|
||
|
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
|
||
|
dnstr = str(dn)[18:-2]
|
||
|
|
||
|
wsgikey = 'SSL_SERVER_%s_DN' % prefix
|
||
|
ssl_environ[wsgikey] = dnstr
|
||
|
|
||
|
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
|
||
|
# for any value to contain slashes itself (in a URL).
|
||
|
while dnstr:
|
||
|
pos = dnstr.rfind('=')
|
||
|
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
|
||
|
pos = dnstr.rfind('/')
|
||
|
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
|
||
|
if key and value:
|
||
|
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
|
||
|
ssl_environ[wsgikey] = value
|
||
|
|
||
|
return ssl_environ
|
||
|
|
||
|
def makefile(self, sock, mode='r', bufsize=-1):
|
||
|
if SSL and isinstance(sock, SSL.ConnectionType):
|
||
|
timeout = sock.gettimeout()
|
||
|
f = SSL_fileobject(sock, mode, bufsize)
|
||
|
f.ssl_timeout = timeout
|
||
|
return f
|
||
|
else:
|
||
|
return wsgiserver.CP_fileobject(sock, mode, bufsize)
|