1 """A high-speed, production ready, thread pooled, generic WSGI server.
2
3 Simplest example on how to use this module directly
4 (without using CherryPy's application machinery):
5
6 from cherrypy import wsgiserver
7
8 def my_crazy_app(environ, start_response):
9 status = '200 OK'
10 response_headers = [('Content-type','text/plain')]
11 start_response(status, response_headers)
12 return ['Hello world!\n']
13
14 # Here we set our application to the script_name '/'
15 wsgi_apps = [('/', my_crazy_app)]
16
17 server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps,
18 server_name='localhost')
19
20 # Want SSL support? Just set these attributes
21 # server.ssl_certificate = <filename>
22 # server.ssl_private_key = <filename>
23
24 if __name__ == '__main__':
25 try:
26 server.start()
27 except KeyboardInterrupt:
28 server.stop()
29
30 This won't call the CherryPy engine (application side) at all, only the
31 WSGI server, which is independant from the rest of CherryPy. Don't
32 let the name "CherryPyWSGIServer" throw you; the name merely reflects
33 its origin, not it's coupling.
34
35 The CherryPy WSGI server can serve as many WSGI applications
36 as you want in one instance:
37
38 wsgi_apps = [('/', my_crazy_app), ('/blog', my_blog_app)]
39
40 """
41
42
43 import base64
44 import Queue
45 import os
46 import re
47 quoted_slash = re.compile("(?i)%2F")
48 import rfc822
49 import socket
50 try:
51 import cStringIO as StringIO
52 except ImportError:
53 import StringIO
54 import sys
55 import threading
56 import time
57 import traceback
58 from urllib import unquote
59 from urlparse import urlparse
60
61 try:
62 from OpenSSL import SSL
63 from OpenSSL import crypto
64 except ImportError:
65 SSL = None
66
67 import errno
68 socket_errors_to_ignore = []
69
70 for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET",
71 "EHOSTDOWN", "EHOSTUNREACH",
72 "WSAECONNABORTED", "WSAECONNREFUSED", "WSAECONNRESET",
73 "WSAENETRESET", "WSAETIMEDOUT"):
74 if _ in dir(errno):
75 socket_errors_to_ignore.append(getattr(errno, _))
76
77 socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys()
78 socket_errors_to_ignore.append("timed out")
79
80 comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
81 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
82 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
83 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
84 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
85 'WWW-AUTHENTICATE']
86
88 """An HTTP Request (and response).
89
90 A single HTTP connection may consist of multiple request/response pairs.
91
92 connection: the HTTP Connection object which spawned this request.
93 rfile: the 'read' fileobject from the connection's socket
94 ready: when True, the request has been parsed and is ready to begin
95 generating the response. When False, signals the calling Connection
96 that the response should not be generated and the connection should
97 close.
98 close_connection: signals the calling Connection that the request
99 should close. This does not imply an error! The client and/or
100 server may each request that the connection be closed.
101 chunked_write: if True, output will be encoded with the "chunked"
102 transfer-coding. This value is set automatically inside
103 send_headers.
104 """
105
107 self.connection = connection
108 self.rfile = self.connection.rfile
109 self.sendall = self.connection.sendall
110 self.environ = connection.environ.copy()
111
112 self.ready = False
113 self.started_response = False
114 self.status = ""
115 self.outheaders = []
116 self.sent_headers = False
117 self.close_connection = False
118 self.chunked_write = False
119
121 """Parse the next HTTP request start-line and message-headers."""
122
123
124
125
126
127
128
129 request_line = self.rfile.readline()
130 if not request_line:
131
132 self.ready = False
133 return
134
135 if request_line == "\r\n":
136
137
138
139
140 request_line = self.rfile.readline()
141 if not request_line:
142 self.ready = False
143 return
144
145 server = self.connection.server
146 environ = self.environ
147 environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version
148
149 method, path, req_protocol = request_line.strip().split(" ", 2)
150 environ["REQUEST_METHOD"] = method
151
152
153 scheme, location, path, params, qs, frag = urlparse(path)
154
155 if frag:
156 self.simple_response("400 Bad Request",
157 "Illegal #fragment in Request-URI.")
158 return
159
160 if scheme:
161 environ["wsgi.url_scheme"] = scheme
162 if params:
163 path = path + ";" + params
164
165
166
167
168
169
170
171 atoms = [unquote(x) for x in quoted_slash.split(path)]
172 path = "%2F".join(atoms)
173
174 if path == "*":
175
176
177 environ["SCRIPT_NAME"] = ""
178 environ["PATH_INFO"] = "*"
179 self.wsgi_app = server.mount_points[-1][1]
180 else:
181 for mount_point, wsgi_app in server.mount_points:
182
183 if path.startswith(mount_point + "/") or path == mount_point:
184 environ["SCRIPT_NAME"] = mount_point
185 environ["PATH_INFO"] = path[len(mount_point):]
186 self.wsgi_app = wsgi_app
187 break
188 else:
189 self.simple_response("404 Not Found")
190 return
191
192
193
194 environ["QUERY_STRING"] = qs
195
196
197
198
199
200
201
202
203
204
205
206
207
208 rp = int(req_protocol[5]), int(req_protocol[7])
209 sp = int(server.protocol[5]), int(server.protocol[7])
210 if sp[0] != rp[0]:
211 self.simple_response("505 HTTP Version Not Supported")
212 return
213
214 environ["SERVER_PROTOCOL"] = req_protocol
215
216
217
218 environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol
219 self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
220
221
222 if location:
223 environ["SERVER_NAME"] = location
224
225
226 try:
227 self.read_headers()
228 except ValueError, ex:
229 self.simple_response("400 Bad Request", repr(ex.args))
230 return
231
232 creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
233 environ["AUTH_TYPE"] = creds[0]
234 if creds[0].lower() == 'basic':
235 user, pw = base64.decodestring(creds[1]).split(":", 1)
236 environ["REMOTE_USER"] = user
237
238
239 if self.response_protocol == "HTTP/1.1":
240 if environ.get("HTTP_CONNECTION", "") == "close":
241 self.close_connection = True
242 else:
243
244 if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
245 self.close_connection = True
246
247
248 te = None
249 if self.response_protocol == "HTTP/1.1":
250 te = environ.get("HTTP_TRANSFER_ENCODING")
251 if te:
252 te = [x.strip().lower() for x in te.split(",") if x.strip()]
253
254 read_chunked = False
255
256 if te:
257 for enc in te:
258 if enc == "chunked":
259 read_chunked = True
260 else:
261
262
263 self.simple_response("501 Unimplemented")
264 self.close_connection = True
265 return
266
267 if read_chunked:
268 if not self.decode_chunked():
269 return
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288 if environ.get("HTTP_EXPECT", "") == "100-continue":
289 self.simple_response(100)
290
291 self.ready = True
292
294 """Read header lines from the incoming stream."""
295 environ = self.environ
296
297 while True:
298 line = self.rfile.readline()
299 if not line:
300
301 raise ValueError("Illegal end of headers.")
302
303 if line == '\r\n':
304
305 break
306
307 if line[0] in ' \t':
308
309 v = line.strip()
310 else:
311 k, v = line.split(":", 1)
312 k, v = k.strip().upper(), v.strip()
313 envname = "HTTP_" + k.replace("-", "_")
314
315 if k in comma_separated_headers:
316 existing = environ.get(envname)
317 if existing:
318 v = ", ".join((existing, v))
319 environ[envname] = v
320
321 ct = environ.pop("HTTP_CONTENT_TYPE", None)
322 if ct:
323 environ["CONTENT_TYPE"] = ct
324 cl = environ.pop("HTTP_CONTENT_LENGTH", None)
325 if cl:
326 environ["CONTENT_LENGTH"] = cl
327
329 """Decode the 'chunked' transfer coding."""
330 cl = 0
331 data = StringIO.StringIO()
332 while True:
333 line = self.rfile.readline().strip().split(";", 1)
334 chunk_size = int(line.pop(0), 16)
335 if chunk_size <= 0:
336 break
337
338 cl += chunk_size
339 data.write(self.rfile.read(chunk_size))
340 crlf = self.rfile.read(2)
341 if crlf != "\r\n":
342 self.simple_response("400 Bad Request",
343 "Bad chunked transfer coding "
344 "(expected '\\r\\n', got %r)" % crlf)
345 return
346
347
348 self.read_headers()
349
350 data.seek(0)
351 self.environ["wsgi.input"] = data
352 self.environ["CONTENT_LENGTH"] = str(cl) or ""
353 return True
354
377
379 """Write a simple response back to the client."""
380 status = str(status)
381 buf = ["%s %s\r\n" % (self.connection.server.protocol, status),
382 "Content-Length: %s\r\n" % len(msg)]
383
384 if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
385
386 self.close_connection = True
387 buf.append("Connection: close\r\n")
388
389 buf.append("\r\n")
390 if msg:
391 buf.append(msg)
392 self.sendall("".join(buf))
393
395 """WSGI callable to begin the HTTP response."""
396 if self.started_response:
397 if not exc_info:
398 raise AssertionError("WSGI start_response called a second "
399 "time with no exc_info.")
400 else:
401 try:
402 raise exc_info[0], exc_info[1], exc_info[2]
403 finally:
404 exc_info = None
405 self.started_response = True
406 self.status = status
407 self.outheaders.extend(headers)
408 return self.write
409
411 """WSGI callable to write unbuffered data to the client.
412
413 This method is also used internally by start_response (to write
414 data from the iterable returned by the WSGI application).
415 """
416 if not self.started_response:
417 raise AssertionError("WSGI write called before start_response.")
418
419 if not self.sent_headers:
420 self.sent_headers = True
421 self.send_headers()
422
423 if self.chunked_write and chunk:
424 buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
425 self.sendall("".join(buf))
426 else:
427 self.sendall(chunk)
428
430 """Assert, process, and send the HTTP response message-headers."""
431 hkeys = [key.lower() for key, value in self.outheaders]
432 status = int(self.status[:3])
433
434 if status == 413:
435
436 self.close_connection = True
437 elif "content-length" not in hkeys:
438
439
440
441 if status < 200 or status in (204, 205, 304):
442 pass
443 else:
444 if self.response_protocol == 'HTTP/1.1':
445
446 self.chunked_write = True
447 self.outheaders.append(("Transfer-Encoding", "chunked"))
448 else:
449
450 self.close_connection = True
451
452 if "connection" not in hkeys:
453 if self.response_protocol == 'HTTP/1.1':
454 if self.close_connection:
455 self.outheaders.append(("Connection", "close"))
456 else:
457 if not self.close_connection:
458 self.outheaders.append(("Connection", "Keep-Alive"))
459
460 if "date" not in hkeys:
461 self.outheaders.append(("Date", rfc822.formatdate()))
462
463 server = self.connection.server
464
465 if "server" not in hkeys:
466 self.outheaders.append(("Server", server.version))
467
468 buf = [server.protocol, " ", self.status, "\r\n"]
469 try:
470 buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
471 except TypeError:
472 if not isinstance(k, str):
473 raise TypeError("WSGI response header key %r is not a string.")
474 if not isinstance(v, str):
475 raise TypeError("WSGI response header value %r is not a string.")
476 else:
477 raise
478 buf.append("\r\n")
479 self.sendall("".join(buf))
480
481
483 """Exception raised when a client speaks HTTP to an HTTPS socket."""
484 pass
485
486
488 """Wrap the given method with SSL error-trapping.
489
490 is_reader: if False (the default), EOF errors will be raised.
491 If True, EOF errors will return "" (to emulate normal sockets).
492 """
493 def ssl_method_wrapper(self, *args, **kwargs):
494
495 start = time.time()
496 while True:
497 try:
498 return method(self, *args, **kwargs)
499 except (SSL.WantReadError, SSL.WantWriteError):
500
501
502
503
504 time.sleep(self.ssl_retry)
505 except SSL.SysCallError, e:
506 if is_reader and e.args == (-1, 'Unexpected EOF'):
507 return ""
508
509 errno = e.args[0]
510 if is_reader and errno in socket_errors_to_ignore:
511 return ""
512 raise socket.error(errno)
513 except SSL.Error, e:
514 if is_reader and e.args == (-1, 'Unexpected EOF'):
515 return ""
516
517 thirdarg = None
518 try:
519 thirdarg = e.args[0][0][2]
520 except IndexError:
521 pass
522
523 if is_reader and thirdarg == 'ssl handshake failure':
524 return ""
525 if thirdarg == 'http request':
526
527 raise NoSSLError()
528 raise
529 if time.time() - start > self.ssl_timeout:
530 raise socket.timeout("timed out")
531 return ssl_method_wrapper
532
546
547
549 """An HTTP connection (active socket).
550
551 socket: the raw socket object (usually TCP) for this connection.
552 addr: the "bind address" for the remote end of the socket.
553 For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT).
554 For UNIX domain sockets, this will be a string.
555 server: the HTTP Server for this Connection. Usually, the server
556 object possesses a passive (server) socket which spawns multiple,
557 active (client) sockets, one for each connection.
558
559 environ: a WSGI environ template. This will be copied for each request.
560 rfile: a fileobject for reading from the socket.
561 sendall: a function for writing (+ flush) to the socket.
562 """
563
564 rbufsize = -1
565 RequestHandlerClass = HTTPRequest
566 environ = {"wsgi.version": (1, 0),
567 "wsgi.url_scheme": "http",
568 "wsgi.multithread": True,
569 "wsgi.multiprocess": False,
570 "wsgi.run_once": False,
571 "wsgi.errors": sys.stderr,
572 }
573
574 - def __init__(self, sock, addr, server):
610
612 """Read each request and respond appropriately."""
613 try:
614 while True:
615
616
617
618 req = None
619 req = self.RequestHandlerClass(self)
620
621 req.parse_request()
622 if not req.ready:
623 return
624 req.respond()
625 if req.close_connection:
626 return
627 except socket.error, e:
628 errno = e.args[0]
629 if errno not in socket_errors_to_ignore:
630 if req:
631 req.simple_response("500 Internal Server Error",
632 format_exc())
633 return
634 except (KeyboardInterrupt, SystemExit):
635 raise
636 except NoSSLError:
637
638 req.sendall = self.socket._sock.sendall
639 req.simple_response("400 Bad Request",
640 "The client sent a plain HTTP request, but "
641 "this server only speaks HTTPS on this port.")
642 except:
643 if req:
644 req.simple_response("500 Internal Server Error", format_exc())
645
647 """Close the socket underlying this connection."""
648 self.rfile.close()
649 self.socket.close()
650
651
659
660
661 _SHUTDOWNREQUEST = None
662
664 """Thread which continuously polls a Queue for Connection objects.
665
666 server: the HTTP Server which spawned this thread, and which owns the
667 Queue and is placing active connections into it.
668 ready: a simple flag for the calling server to know when this thread
669 has begun polling the Queue.
670
671 Due to the timing issues of polling a Queue, a WorkerThread does not
672 check its own 'ready' flag after it has started. To stop the thread,
673 it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
674 (one for each running WorkerThread).
675 """
676
681
696
697
699 """A thread-safe wrapper for an SSL.Connection.
700
701 *args: the arguments to create the wrapped SSL.Connection(*args).
702 """
703
705 self._ssl_conn = SSL.Connection(*args)
706 self._lock = threading.RLock()
707
708 for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
709 'renegotiate', 'bind', 'listen', 'connect', 'accept',
710 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list',
711 'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
712 'makefile', 'get_app_data', 'set_app_data', 'state_string',
713 'sock_shutdown', 'get_peer_certificate', 'want_read',
714 'want_write', 'set_connect_state', 'set_accept_state',
715 'connect_ex', 'sendall', 'settimeout'):
716 exec """def %s(self, *args):
717 self._lock.acquire()
718 try:
719 return self._ssl_conn.%s(*args)
720 finally:
721 self._lock.release()
722 """ % (f, f)
723
724
726 """An HTTP server for WSGI.
727
728 bind_addr: a (host, port) tuple if TCP sockets are desired;
729 for UNIX sockets, supply the filename as a string.
730 wsgi_app: the WSGI 'application callable'; multiple WSGI applications
731 may be passed as (script_name, callable) pairs.
732 numthreads: the number of worker threads to create (default 10).
733 server_name: the string to set for WSGI's SERVER_NAME environ entry.
734 Defaults to socket.gethostname().
735 max: the maximum number of queued requests (defaults to -1 = no limit).
736 request_queue_size: the 'backlog' argument to socket.listen();
737 specifies the maximum number of queued connections (default 5).
738 timeout: the timeout in seconds for accepted connections (default 10).
739
740 protocol: the version string to write in the Status-Line of all
741 HTTP responses. For example, "HTTP/1.1" (the default). This
742 also limits the supported features used in the response.
743
744
745 SSL/HTTPS
746 ---------
747 The OpenSSL module must be importable for SSL functionality.
748 You can obtain it from http://pyopenssl.sourceforge.net/
749
750 ssl_certificate: the filename of the server SSL certificate.
751 ssl_privatekey: the filename of the server's private key file.
752
753 If either of these is None (both are None by default), this server
754 will not use SSL. If both are given and are valid, they will be read
755 on server start and used in the SSL context for the listening socket.
756 """
757
758 protocol = "HTTP/1.1"
759 version = "CherryPy/3.0.2"
760 ready = False
761 _interrupt = None
762 ConnectionClass = HTTPConnection
763
764
765 ssl_certificate = None
766 ssl_private_key = None
767
768 - def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
769 max=-1, request_queue_size=5, timeout=10):
770 self.requests = Queue.Queue(max)
771
772 if callable(wsgi_app):
773
774
775 self.mount_points = [("", wsgi_app)]
776 else:
777
778
779
780 self.mount_points = wsgi_app
781 self.mount_points.sort()
782 self.mount_points.reverse()
783
784 self.bind_addr = bind_addr
785 self.numthreads = numthreads or 1
786 if not server_name:
787 server_name = socket.gethostname()
788 self.server_name = server_name
789 self.request_queue_size = request_queue_size
790 self._workerThreads = []
791
792 self.timeout = timeout
793
795 """Run the server forever."""
796
797
798
799
800 self._interrupt = None
801
802
803 if isinstance(self.bind_addr, basestring):
804
805
806
807 try: os.unlink(self.bind_addr)
808 except: pass
809
810
811 try: os.chmod(self.bind_addr, 0777)
812 except: pass
813
814 info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
815 else:
816
817
818 host, port = self.bind_addr
819 flags = 0
820 if host == '':
821
822
823
824
825
826
827
828
829 host = None
830 flags = socket.AI_PASSIVE
831 try:
832 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
833 socket.SOCK_STREAM, 0, flags)
834 except socket.gaierror:
835
836 info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)]
837
838 self.socket = None
839 msg = "No socket could be created"
840 for res in info:
841 af, socktype, proto, canonname, sa = res
842 try:
843 self.bind(af, socktype, proto)
844 except socket.error, msg:
845 if self.socket:
846 self.socket.close()
847 self.socket = None
848 continue
849 break
850 if not self.socket:
851 raise socket.error, msg
852
853
854 self.socket.settimeout(1)
855 self.socket.listen(self.request_queue_size)
856
857
858 for i in xrange(self.numthreads):
859 self._workerThreads.append(WorkerThread(self))
860 for worker in self._workerThreads:
861 worker.setName("CP WSGIServer " + worker.getName())
862 worker.start()
863 for worker in self._workerThreads:
864 while not worker.ready:
865 time.sleep(.1)
866
867 self.ready = True
868 while self.ready:
869 self.tick()
870 if self.interrupt:
871 while self.interrupt is True:
872
873 time.sleep(0.1)
874 raise self.interrupt
875
876 - def bind(self, family, type, proto=0):
877 """Create (or recreate) the actual socket object."""
878 self.socket = socket.socket(family, type, proto)
879 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
880
881 if self.ssl_certificate and self.ssl_private_key:
882 if SSL is None:
883 raise ImportError("You must install pyOpenSSL to use HTTPS.")
884
885
886 ctx = SSL.Context(SSL.SSLv23_METHOD)
887 ctx.use_privatekey_file(self.ssl_private_key)
888 ctx.use_certificate_file(self.ssl_certificate)
889 self.socket = SSLConnection(ctx, self.socket)
890 self.populate_ssl_environ()
891 self.socket.bind(self.bind_addr)
892
894 """Accept a new connection and put it on the Queue."""
895 try:
896 s, addr = self.socket.accept()
897 if not self.ready:
898 return
899 if hasattr(s, 'settimeout'):
900 s.settimeout(self.timeout)
901 conn = self.ConnectionClass(s, addr, self)
902 self.requests.put(conn)
903 except socket.timeout:
904
905
906
907 return
908 except socket.error, x:
909 msg = x.args[1]
910 if msg in ("Bad file descriptor", "Socket operation on non-socket"):
911
912 return
913 if msg == "Resource temporarily unavailable":
914
915 return
916 raise
917
924 interrupt = property(_get_interrupt, _set_interrupt,
925 doc="Set this to an Exception instance to "
926 "interrupt the server.")
927
929 """Gracefully shutdown a server that is serving forever."""
930 self.ready = False
931
932 sock = getattr(self, "socket", None)
933 if sock:
934 if not isinstance(self.bind_addr, basestring):
935
936 try:
937 host, port = sock.getsockname()[:2]
938 except socket.error, x:
939 if x.args[1] != "Bad file descriptor":
940 raise
941 else:
942
943
944
945
946 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
947 socket.SOCK_STREAM):
948 af, socktype, proto, canonname, sa = res
949 s = None
950 try:
951 s = socket.socket(af, socktype, proto)
952
953
954 s.settimeout(1.0)
955 s.connect((host, port))
956 s.close()
957 except socket.error:
958 if s:
959 s.close()
960 if hasattr(sock, "close"):
961 sock.close()
962 self.socket = None
963
964
965
966 for worker in self._workerThreads:
967 self.requests.put(_SHUTDOWNREQUEST)
968
969
970 current = threading.currentThread()
971 while self._workerThreads:
972 worker = self._workerThreads.pop()
973 if worker is not current and worker.isAlive:
974 try:
975 worker.join()
976 except AssertionError:
977 pass
978
980 """Create WSGI environ entries to be merged into each request."""
981 cert = open(self.ssl_certificate).read()
982 cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
983 self.ssl_environ = {
984
985
986
987
988
989 }
990
991
992 self.ssl_environ.update({
993 'SSL_SERVER_M_VERSION': cert.get_version(),
994 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
995
996
997 })
998
999 for prefix, dn in [("I", cert.get_issuer()),
1000 ("S", cert.get_subject())]:
1001
1002
1003
1004 dnstr = str(dn)[18:-2]
1005
1006 wsgikey = 'SSL_SERVER_%s_DN' % prefix
1007 self.ssl_environ[wsgikey] = dnstr
1008
1009
1010
1011 while dnstr:
1012 pos = dnstr.rfind("=")
1013 dnstr, value = dnstr[:pos], dnstr[pos + 1:]
1014 pos = dnstr.rfind("/")
1015 dnstr, key = dnstr[:pos], dnstr[pos + 1:]
1016 if key and value:
1017 wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
1018 self.ssl_environ[wsgikey] = value
1019