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