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
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
64
65
66
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
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):
112
113 - def send(self, *args, **kwargs):
116
117
119 """A thread-safe wrapper for an SSL.Connection.
120
121 *args: the arguments to create the wrapped SSL.Connection(*args).
122 """
123
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
145 self._lock.acquire()
146 try:
147
148 return self._ssl_conn.shutdown()
149 finally:
150 self._lock.release()
151
152
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):
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
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
202 """Return WSGI environ entries to be merged into each request."""
203 ssl_environ = {
204 "HTTPS": "on",
205
206
207
208
209
210 }
211
212 if self.certificate:
213
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
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