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
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
65
66
67
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
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):
102
103 - def sendall(self, *args, **kwargs):
106
107 - def send(self, *args, **kwargs):
110
111
113
114 """A thread-safe wrapper for an SSL.Connection.
115
116 ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
117 """
118
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
140 self._lock.acquire()
141 try:
142
143 return self._ssl_conn.shutdown()
144 finally:
145 self._lock.release()
146
147
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):
176
177 - def bind(self, sock):
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
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
200 """Return WSGI environ entries to be merged into each request."""
201 ssl_environ = {
202 "HTTPS": "on",
203
204
205
206
207
208 }
209
210 if self.certificate:
211
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
218
219
220
221 })
222
223 for prefix, dn in [("I", cert.get_issuer()),
224 ("S", cert.get_subject())]:
225
226
227
228 dnstr = str(dn)[18:-2]
229
230 wsgikey = 'SSL_SERVER_%s_DN' % prefix
231 ssl_environ[wsgikey] = dnstr
232
233
234
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):
254