0001"""A high-speed, production ready, thread pooled, generic WSGI server.
0002
0003Simplest example on how to use this module directly
0004(without using CherryPy's application machinery):
0005
0006 from cherrypy import wsgiserver
0007
0008 def my_crazy_app(environ, start_response):
0009 status = '200 OK'
0010 response_headers = [('Content-type','text/plain')]
0011 start_response(status, response_headers)
0012 return ['Hello world!\n']
0013
0014 # Here we set our application to the script_name '/'
0015 wsgi_apps = [('/', my_crazy_app)]
0016
0017 server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps,
0018 server_name='localhost')
0019
0020 # Want SSL support? Just set these attributes
0021 # server.ssl_certificate = <filename>
0022 # server.ssl_private_key = <filename>
0023
0024 if __name__ == '__main__':
0025 try:
0026 server.start()
0027 except KeyboardInterrupt:
0028 server.stop()
0029
0030This won't call the CherryPy engine (application side) at all, only the
0031WSGI server, which is independant from the rest of CherryPy. Don't
0032let the name "CherryPyWSGIServer" throw you; the name merely reflects
0033its origin, not it's coupling.
0034
0035The CherryPy WSGI server can serve as many WSGI applications
0036as you want in one instance:
0037
0038 wsgi_apps = [('/', my_crazy_app), ('/blog', my_blog_app)]
0039
0040"""
0041
0042
0043import base64
0044import Queue
0045import os
0046import re
0047quoted_slash = re.compile("(?i)%2F")
0048import rfc822
0049import socket
0050try:
0051 import cStringIO as StringIO
0052except ImportError:
0053 import StringIO
0054import sys
0055import threading
0056import time
0057import traceback
0058from urllib import unquote
0059from urlparse import urlparse
0060
0061try:
0062 from OpenSSL import SSL
0063 from OpenSSL import crypto
0064except ImportError:
0065 SSL = None
0066
0067import errno
0068socket_errors_to_ignore = []
0069
0070for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET",
0071 "EHOSTDOWN", "EHOSTUNREACH",
0072 "WSAECONNABORTED", "WSAECONNREFUSED", "WSAECONNRESET",
0073 "WSAENETRESET", "WSAETIMEDOUT"):
0074 if _ in dir(errno):
0075 socket_errors_to_ignore.append(getattr(errno, _))
0076
0077socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys()
0078socket_errors_to_ignore.append("timed out")
0079
0080comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
0081 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
0082 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
0083 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
0084 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
0085 'WWW-AUTHENTICATE']
0086
0087class HTTPRequest(object):
0088 """An HTTP Request (and response).
0089
0090 A single HTTP connection may consist of multiple request/response pairs.
0091
0092 connection: the HTTP Connection object which spawned this request.
0093 rfile: the 'read' fileobject from the connection's socket
0094 ready: when True, the request has been parsed and is ready to begin
0095 generating the response. When False, signals the calling Connection
0096 that the response should not be generated and the connection should
0097 close.
0098 close_connection: signals the calling Connection that the request
0099 should close. This does not imply an error! The client and/or
0100 server may each request that the connection be closed.
0101 chunked_write: if True, output will be encoded with the "chunked"
0102 transfer-coding. This value is set automatically inside
0103 send_headers.
0104 """
0105
0106 def __init__(self, connection):
0107 self.connection = connection
0108 self.rfile = self.connection.rfile
0109 self.sendall = self.connection.sendall
0110 self.environ = connection.environ.copy()
0111
0112 self.ready = False
0113 self.started_response = False
0114 self.status = ""
0115 self.outheaders = []
0116 self.sent_headers = False
0117 self.close_connection = False
0118 self.chunked_write = False
0119
0120 def parse_request(self):
0121 """Parse the next HTTP request start-line and message-headers."""
0122
0123
0124
0125
0126
0127
0128
0129 request_line = self.rfile.readline()
0130 if not request_line:
0131
0132 self.ready = False
0133 return
0134
0135 if request_line == "\r\n":
0136
0137
0138
0139
0140 request_line = self.rfile.readline()
0141 if not request_line:
0142 self.ready = False
0143 return
0144
0145 server = self.connection.server
0146 environ = self.environ
0147 environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version
0148
0149 method, path, req_protocol = request_line.strip().split(" ", 2)
0150 environ["REQUEST_METHOD"] = method
0151
0152
0153 scheme, location, path, params, qs, frag = urlparse(path)
0154
0155 if frag:
0156 self.simple_response("400 Bad Request",
0157 "Illegal #fragment in Request-URI.")
0158 return
0159
0160 if scheme:
0161 environ["wsgi.url_scheme"] = scheme
0162 if params:
0163 path = path + ";" + params
0164
0165
0166
0167
0168
0169
0170
0171 atoms = [unquote(x) for x in quoted_slash.split(path)]
0172 path = "%2F".join(atoms)
0173
0174 if path == "*":
0175
0176
0177 environ["SCRIPT_NAME"] = ""
0178 environ["PATH_INFO"] = "*"
0179 self.wsgi_app = server.mount_points[-1][1]
0180 else:
0181 for mount_point, wsgi_app in server.mount_points:
0182
0183 if path.startswith(mount_point + "/") or path == mount_point:
0184 environ["SCRIPT_NAME"] = mount_point
0185 environ["PATH_INFO"] = path[len(mount_point):]
0186 self.wsgi_app = wsgi_app
0187 break
0188 else:
0189 self.simple_response("404 Not Found")
0190 return
0191
0192
0193
0194 environ["QUERY_STRING"] = qs
0195
0196
0197
0198
0199
0200
0201
0202
0203
0204
0205
0206
0207
0208 rp = int(req_protocol[5]), int(req_protocol[7])
0209 sp = int(server.protocol[5]), int(server.protocol[7])
0210 if sp[0] != rp[0]:
0211 self.simple_response("505 HTTP Version Not Supported")
0212 return
0213
0214 environ["SERVER_PROTOCOL"] = req_protocol
0215
0216
0217
0218 environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol
0219 self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
0220
0221
0222 if location:
0223 environ["SERVER_NAME"] = location
0224
0225
0226 try:
0227 self.read_headers()
0228 except ValueError, ex:
0229 self.simple_response("400 Bad Request", repr(ex.args))
0230 return
0231
0232 creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
0233 environ["AUTH_TYPE"] = creds[0]
0234 if creds[0].lower() == 'basic':
0235 user, pw = base64.decodestring(creds[1]).split(":", 1)
0236 environ["REMOTE_USER"] = user
0237
0238
0239 if self.response_protocol == "HTTP/1.1":
0240 if environ.get("HTTP_CONNECTION", "") == "close":
0241 self.close_connection = True
0242 else:
0243
0244 if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
0245 self.close_connection = True
0246
0247
0248 te = None
0249 if self.response_protocol == "HTTP/1.1":
0250 te = environ.get("HTTP_TRANSFER_ENCODING")
0251 if te:
0252 te = [x.strip().lower() for x in te.split(",") if x.strip()]
0253
0254 read_chunked = False
0255
0256 if te:
0257 for enc in te:
0258 if enc == "chunked":
0259 read_chunked = True
0260 else:
0261
0262
0263 self.simple_response("501 Unimplemented")
0264 self.close_connection = True
0265 return
0266
0267 if read_chunked:
0268 if not self.decode_chunked():
0269 return
0270
0271
0272
0273
0274
0275
0276
0277
0278
0279
0280
0281
0282
0283
0284
0285
0286
0287
0288 if environ.get("HTTP_EXPECT", "") == "100-continue":
0289 self.simple_response(100)
0290
0291 self.ready = True
0292
0293 def read_headers(self):
0294 """Read header lines from the incoming stream."""
0295 environ = self.environ
0296
0297 while True:
0298 line = self.rfile.readline()
0299 if not line:
0300
0301 raise ValueError("Illegal end of headers.")
0302
0303 if line == '\r\n':
0304
0305 break
0306
0307 if line[0] in ' \t':
0308
0309 v = line.strip()
0310 else:
0311 k, v = line.split(":", 1)
0312 k, v = k.strip().upper(), v.strip()
0313 envname = "HTTP_" + k.replace("-", "_")
0314
0315 if k in comma_separated_headers:
0316 existing = environ.get(envname)
0317 if existing:
0318 v = ", ".join((existing, v))
0319 environ[envname] = v
0320
0321 ct = environ.pop("HTTP_CONTENT_TYPE", None)
0322 if ct:
0323 environ["CONTENT_TYPE"] = ct
0324 cl = environ.pop("HTTP_CONTENT_LENGTH", None)
0325 if cl:
0326 environ["CONTENT_LENGTH"] = cl
0327
0328 def decode_chunked(self):
0329 """Decode the 'chunked' transfer coding."""
0330 cl = 0
0331 data = StringIO.StringIO()
0332 while True:
0333 line = self.rfile.readline().strip().split(";", 1)
0334 chunk_size = int(line.pop(0), 16)
0335 if chunk_size <= 0:
0336 break
0337
0338 cl += chunk_size
0339 data.write(self.rfile.read(chunk_size))
0340 crlf = self.rfile.read(2)
0341 if crlf != "\r\n":
0342 self.simple_response("400 Bad Request",
0343 "Bad chunked transfer coding "
0344 "(expected '\\r\\n', got %r)" % crlf)
0345 return
0346
0347
0348 self.read_headers()
0349
0350 data.seek(0)
0351 self.environ["wsgi.input"] = data
0352 self.environ["CONTENT_LENGTH"] = str(cl) or ""
0353 return True
0354
0355 def respond(self):
0356 """Call the appropriate WSGI app and write its iterable output."""
0357 response = self.wsgi_app(self.environ, self.start_response)
0358 try:
0359 for chunk in response:
0360
0361
0362
0363
0364
0365
0366 if chunk:
0367 self.write(chunk)
0368 finally:
0369 if hasattr(response, "close"):
0370 response.close()
0371 if (self.ready and not self.sent_headers
0372 and not self.connection.server.interrupt):
0373 self.sent_headers = True
0374 self.send_headers()
0375 if self.chunked_write:
0376 self.sendall("0\r\n\r\n")
0377
0378 def simple_response(self, status, msg=""):
0379 """Write a simple response back to the client."""
0380 status = str(status)
0381 buf = ["%s %s\r\n" % (self.connection.server.protocol, status),
0382 "Content-Length: %s\r\n" % len(msg)]
0383
0384 if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
0385
0386 self.close_connection = True
0387 buf.append("Connection: close\r\n")
0388
0389 buf.append("\r\n")
0390 if msg:
0391 buf.append(msg)
0392 self.sendall("".join(buf))
0393
0394 def start_response(self, status, headers, exc_info = None):
0395 """WSGI callable to begin the HTTP response."""
0396 if self.started_response:
0397 if not exc_info:
0398 raise AssertionError("WSGI start_response called a second "
0399 "time with no exc_info.")
0400 else:
0401 try:
0402 raise exc_info[0], exc_info[1], exc_info[2]
0403 finally:
0404 exc_info = None
0405 self.started_response = True
0406 self.status = status
0407 self.outheaders.extend(headers)
0408 return self.write
0409
0410 def write(self, chunk):
0411 """WSGI callable to write unbuffered data to the client.
0412
0413 This method is also used internally by start_response (to write
0414 data from the iterable returned by the WSGI application).
0415 """
0416 if not self.started_response:
0417 raise AssertionError("WSGI write called before start_response.")
0418
0419 if not self.sent_headers:
0420 self.sent_headers = True
0421 self.send_headers()
0422
0423 if self.chunked_write and chunk:
0424 buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
0425 self.sendall("".join(buf))
0426 else:
0427 self.sendall(chunk)
0428
0429 def send_headers(self):
0430 """Assert, process, and send the HTTP response message-headers."""
0431 hkeys = [key.lower() for key, value in self.outheaders]
0432 status = int(self.status[:3])
0433
0434 if status == 413:
0435
0436 self.close_connection = True
0437 elif "content-length" not in hkeys:
0438
0439
0440
0441 if status < 200 or status in (204, 205, 304):
0442 pass
0443 else:
0444 if self.response_protocol == 'HTTP/1.1':
0445
0446 self.chunked_write = True
0447 self.outheaders.append(("Transfer-Encoding", "chunked"))
0448 else:
0449
0450 self.close_connection = True
0451
0452 if "connection" not in hkeys:
0453 if self.response_protocol == 'HTTP/1.1':
0454 if self.close_connection:
0455 self.outheaders.append(("Connection", "close"))
0456 else:
0457 if not self.close_connection:
0458 self.outheaders.append(("Connection", "Keep-Alive"))
0459
0460 if "date" not in hkeys:
0461 self.outheaders.append(("Date", rfc822.formatdate()))
0462
0463 server = self.connection.server
0464
0465 if "server" not in hkeys:
0466 self.outheaders.append(("Server", server.version))
0467
0468 buf = [server.protocol, " ", self.status, "\r\n"]
0469 try:
0470 buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
0471 except TypeError:
0472 if not isinstance(k, str):
0473 raise TypeError("WSGI response header key %r is not a string.")
0474 if not isinstance(v, str):
0475 raise TypeError("WSGI response header value %r is not a string.")
0476 else:
0477 raise
0478 buf.append("\r\n")
0479 self.sendall("".join(buf))
0480
0481
0482class NoSSLError(Exception):
0483 """Exception raised when a client speaks HTTP to an HTTPS socket."""
0484 pass
0485
0486
0487def _ssl_wrap_method(method, is_reader=False):
0488 """Wrap the given method with SSL error-trapping.
0489
0490 is_reader: if False (the default), EOF errors will be raised.
0491 If True, EOF errors will return "" (to emulate normal sockets).
0492 """
0493 def ssl_method_wrapper(self, *args, **kwargs):
0494
0495 start = time.time()
0496 while True:
0497 try:
0498 return method(self, *args, **kwargs)
0499 except (SSL.WantReadError, SSL.WantWriteError):
0500
0501
0502
0503
0504 time.sleep(self.ssl_retry)
0505 except SSL.SysCallError, e:
0506 if is_reader and e.args == (-1, 'Unexpected EOF'):
0507 return ""
0508
0509 errno = e.args[0]
0510 if is_reader and errno in socket_errors_to_ignore:
0511 return ""
0512 raise socket.error(errno)
0513 except SSL.Error, e:
0514 if is_reader and e.args == (-1, 'Unexpected EOF'):
0515 return ""
0516
0517 thirdarg = None
0518 try:
0519 thirdarg = e.args[0][0][2]
0520 except IndexError:
0521 pass
0522
0523 if is_reader and thirdarg == 'ssl handshake failure':
0524 return ""
0525 if thirdarg == 'http request':
0526
0527 raise NoSSLError()
0528 raise
0529 if time.time() - start > self.ssl_timeout:
0530 raise socket.timeout("timed out")
0531 return ssl_method_wrapper
0532
0533class SSL_fileobject(socket._fileobject):
0534 """Faux file object attached to a socket object."""
0535
0536 ssl_timeout = 3
0537 ssl_retry = .01
0538
0539 close = _ssl_wrap_method(socket._fileobject.close)
0540 flush = _ssl_wrap_method(socket._fileobject.flush)
0541 write = _ssl_wrap_method(socket._fileobject.write)
0542 writelines = _ssl_wrap_method(socket._fileobject.writelines)
0543 read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
0544 readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
0545 readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
0546
0547
0548class HTTPConnection(object):
0549 """An HTTP connection (active socket).
0550
0551 socket: the raw socket object (usually TCP) for this connection.
0552 addr: the "bind address" for the remote end of the socket.
0553 For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT).
0554 For UNIX domain sockets, this will be a string.
0555 server: the HTTP Server for this Connection. Usually, the server
0556 object possesses a passive (server) socket which spawns multiple,
0557 active (client) sockets, one for each connection.
0558
0559 environ: a WSGI environ template. This will be copied for each request.
0560 rfile: a fileobject for reading from the socket.
0561 sendall: a function for writing (+ flush) to the socket.
0562 """
0563
0564 rbufsize = -1
0565 RequestHandlerClass = HTTPRequest
0566 environ = {"wsgi.version": (1, 0),
0567 "wsgi.url_scheme": "http",
0568 "wsgi.multithread": True,
0569 "wsgi.multiprocess": False,
0570 "wsgi.run_once": False,
0571 "wsgi.errors": sys.stderr,
0572 }
0573
0574 def __init__(self, sock, addr, server):
0575 self.socket = sock
0576 self.addr = addr
0577 self.server = server
0578
0579
0580 self.environ = self.environ.copy()
0581
0582 if SSL and isinstance(sock, SSL.ConnectionType):
0583 timeout = sock.gettimeout()
0584 self.rfile = SSL_fileobject(sock, "r", self.rbufsize)
0585 self.rfile.ssl_timeout = timeout
0586 self.sendall = _ssl_wrap_method(sock.sendall)
0587 self.environ["wsgi.url_scheme"] = "https"
0588 self.environ["HTTPS"] = "on"
0589 sslenv = getattr(server, "ssl_environ", None)
0590 if sslenv:
0591 self.environ.update(sslenv)
0592 else:
0593 self.rfile = sock.makefile("rb", self.rbufsize)
0594 self.sendall = sock.sendall
0595
0596 self.environ.update({"wsgi.input": self.rfile,
0597 "SERVER_NAME": self.server.server_name,
0598 })
0599
0600 if isinstance(self.server.bind_addr, basestring):
0601
0602
0603 self.environ["SERVER_PORT"] = ""
0604 else:
0605 self.environ["SERVER_PORT"] = str(self.server.bind_addr[1])
0606
0607
0608 self.environ["REMOTE_ADDR"] = self.addr[0]
0609 self.environ["REMOTE_PORT"] = str(self.addr[1])
0610
0611 def communicate(self):
0612 """Read each request and respond appropriately."""
0613 try:
0614 while True:
0615
0616
0617
0618 req = None
0619 req = self.RequestHandlerClass(self)
0620
0621 req.parse_request()
0622 if not req.ready:
0623 return
0624 req.respond()
0625 if req.close_connection:
0626 return
0627 except socket.error, e:
0628 errno = e.args[0]
0629 if errno not in socket_errors_to_ignore:
0630 if req:
0631 req.simple_response("500 Internal Server Error",
0632 format_exc())
0633 return
0634 except (KeyboardInterrupt, SystemExit):
0635 raise
0636 except NoSSLError:
0637
0638 req.sendall = self.socket._sock.sendall
0639 req.simple_response("400 Bad Request",
0640 "The client sent a plain HTTP request, but "
0641 "this server only speaks HTTPS on this port.")
0642 except:
0643 if req:
0644 req.simple_response("500 Internal Server Error", format_exc())
0645
0646 def close(self):
0647 """Close the socket underlying this connection."""
0648 self.rfile.close()
0649 self.socket.close()
0650
0651
0652def format_exc(limit=None):
0653 """Like print_exc() but return a string. Backport for Python 2.3."""
0654 try:
0655 etype, value, tb = sys.exc_info()
0656 return ''.join(traceback.format_exception(etype, value, tb, limit))
0657 finally:
0658 etype = value = tb = None
0659
0660
0661_SHUTDOWNREQUEST = None
0662
0663class WorkerThread(threading.Thread):
0664 """Thread which continuously polls a Queue for Connection objects.
0665
0666 server: the HTTP Server which spawned this thread, and which owns the
0667 Queue and is placing active connections into it.
0668 ready: a simple flag for the calling server to know when this thread
0669 has begun polling the Queue.
0670
0671 Due to the timing issues of polling a Queue, a WorkerThread does not
0672 check its own 'ready' flag after it has started. To stop the thread,
0673 it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
0674 (one for each running WorkerThread).
0675 """
0676
0677 def __init__(self, server):
0678 self.ready = False
0679 self.server = server
0680 threading.Thread.__init__(self)
0681
0682 def run(self):
0683 try:
0684 self.ready = True
0685 while True:
0686 conn = self.server.requests.get()
0687 if conn is _SHUTDOWNREQUEST:
0688 return
0689
0690 try:
0691 conn.communicate()
0692 finally:
0693 conn.close()
0694 except (KeyboardInterrupt, SystemExit), exc:
0695 self.server.interrupt = exc
0696
0697
0698class SSLConnection:
0699 """A thread-safe wrapper for an SSL.Connection.
0700
0701 *args: the arguments to create the wrapped SSL.Connection(*args).
0702 """
0703
0704 def __init__(self, *args):
0705 self._ssl_conn = SSL.Connection(*args)
0706 self._lock = threading.RLock()
0707
0708 for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
0709 'renegotiate', 'bind', 'listen', 'connect', 'accept',
0710 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list',
0711 'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
0712 'makefile', 'get_app_data', 'set_app_data', 'state_string',
0713 'sock_shutdown', 'get_peer_certificate', 'want_read',
0714 'want_write', 'set_connect_state', 'set_accept_state',
0715 'connect_ex', 'sendall', 'settimeout'):
0716 exec """def %s(self, *args):
0717 self._lock.acquire()
0718 try:
0719 return self._ssl_conn.%s(*args)
0720 finally:
0721 self._lock.release()
0722""" % (f, f)
0723
0724
0725class CherryPyWSGIServer(object):
0726 """An HTTP server for WSGI.
0727
0728 bind_addr: a (host, port) tuple if TCP sockets are desired;
0729 for UNIX sockets, supply the filename as a string.
0730 wsgi_app: the WSGI 'application callable'; multiple WSGI applications
0731 may be passed as (script_name, callable) pairs.
0732 numthreads: the number of worker threads to create (default 10).
0733 server_name: the string to set for WSGI's SERVER_NAME environ entry.
0734 Defaults to socket.gethostname().
0735 max: the maximum number of queued requests (defaults to -1 = no limit).
0736 request_queue_size: the 'backlog' argument to socket.listen();
0737 specifies the maximum number of queued connections (default 5).
0738 timeout: the timeout in seconds for accepted connections (default 10).
0739
0740 protocol: the version string to write in the Status-Line of all
0741 HTTP responses. For example, "HTTP/1.1" (the default). This
0742 also limits the supported features used in the response.
0743
0744
0745 SSL/HTTPS
0746 ---------
0747 The OpenSSL module must be importable for SSL functionality.
0748 You can obtain it from http://pyopenssl.sourceforge.net/
0749
0750 ssl_certificate: the filename of the server SSL certificate.
0751 ssl_privatekey: the filename of the server's private key file.
0752
0753 If either of these is None (both are None by default), this server
0754 will not use SSL. If both are given and are valid, they will be read
0755 on server start and used in the SSL context for the listening socket.
0756 """
0757
0758 protocol = "HTTP/1.1"
0759 version = "CherryPy/3.0.2"
0760 ready = False
0761 _interrupt = None
0762 ConnectionClass = HTTPConnection
0763
0764
0765 ssl_certificate = None
0766 ssl_private_key = None
0767
0768 def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
0769 max=-1, request_queue_size=5, timeout=10):
0770 self.requests = Queue.Queue(max)
0771
0772 if callable(wsgi_app):
0773
0774
0775 self.mount_points = [("", wsgi_app)]
0776 else:
0777
0778
0779
0780 self.mount_points = wsgi_app
0781 self.mount_points.sort()
0782 self.mount_points.reverse()
0783
0784 self.bind_addr = bind_addr
0785 self.numthreads = numthreads or 1
0786 if not server_name:
0787 server_name = socket.gethostname()
0788 self.server_name = server_name
0789 self.request_queue_size = request_queue_size
0790 self._workerThreads = []
0791
0792 self.timeout = timeout
0793
0794 def start(self):
0795 """Run the server forever."""
0796
0797
0798
0799
0800 self._interrupt = None
0801
0802
0803 if isinstance(self.bind_addr, basestring):
0804
0805
0806
0807 try: os.unlink(self.bind_addr)
0808 except: pass
0809
0810
0811 try: os.chmod(self.bind_addr, 0777)
0812 except: pass
0813
0814 info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
0815 else:
0816
0817
0818 host, port = self.bind_addr
0819 flags = 0
0820 if host == '':
0821
0822
0823
0824
0825
0826
0827
0828
0829 host = None
0830 flags = socket.AI_PASSIVE
0831 try:
0832 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
0833 socket.SOCK_STREAM, 0, flags)
0834 except socket.gaierror:
0835
0836 info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)]
0837
0838 self.socket = None
0839 msg = "No socket could be created"
0840 for res in info:
0841 af, socktype, proto, canonname, sa = res
0842 try:
0843 self.bind(af, socktype, proto)
0844 except socket.error, msg:
0845 if self.socket:
0846 self.socket.close()
0847 self.socket = None
0848 continue
0849 break
0850 if not self.socket:
0851 raise socket.error, msg
0852
0853
0854 self.socket.settimeout(1)
0855 self.socket.listen(self.request_queue_size)
0856
0857
0858 for i in xrange(self.numthreads):
0859 self._workerThreads.append(WorkerThread(self))
0860 for worker in self._workerThreads:
0861 worker.setName("CP WSGIServer " + worker.getName())
0862 worker.start()
0863 for worker in self._workerThreads:
0864 while not worker.ready:
0865 time.sleep(.1)
0866
0867 self.ready = True
0868 while self.ready:
0869 self.tick()
0870 if self.interrupt:
0871 while self.interrupt is True:
0872
0873 time.sleep(0.1)
0874 raise self.interrupt
0875
0876 def bind(self, family, type, proto=0):
0877 """Create (or recreate) the actual socket object."""
0878 self.socket = socket.socket(family, type, proto)
0879 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
0880
0881 if self.ssl_certificate and self.ssl_private_key:
0882 if SSL is None:
0883 raise ImportError("You must install pyOpenSSL to use HTTPS.")
0884
0885
0886 ctx = SSL.Context(SSL.SSLv23_METHOD)
0887 ctx.use_privatekey_file(self.ssl_private_key)
0888 ctx.use_certificate_file(self.ssl_certificate)
0889 self.socket = SSLConnection(ctx, self.socket)
0890 self.populate_ssl_environ()
0891 self.socket.bind(self.bind_addr)
0892
0893 def tick(self):
0894 """Accept a new connection and put it on the Queue."""
0895 try:
0896 s, addr = self.socket.accept()
0897 if not self.ready:
0898 return
0899 if hasattr(s, 'settimeout'):
0900 s.settimeout(self.timeout)
0901 conn = self.ConnectionClass(s, addr, self)
0902 self.requests.put(conn)
0903 except socket.timeout:
0904
0905
0906
0907 return
0908 except socket.error, x:
0909 msg = x.args[1]
0910 if msg in ("Bad file descriptor", "Socket operation on non-socket"):
0911
0912 return
0913 if msg == "Resource temporarily unavailable":
0914
0915 return
0916 raise
0917
0918 def _get_interrupt(self):
0919 return self._interrupt
0920 def _set_interrupt(self, interrupt):
0921 self._interrupt = True
0922 self.stop()
0923 self._interrupt = interrupt
0924 interrupt = property(_get_interrupt, _set_interrupt,
0925 doc="Set this to an Exception instance to "
0926 "interrupt the server.")
0927
0928 def stop(self):
0929 """Gracefully shutdown a server that is serving forever."""
0930 self.ready = False
0931
0932 sock = getattr(self, "socket", None)
0933 if sock:
0934 if not isinstance(self.bind_addr, basestring):
0935
0936 try:
0937 host, port = sock.getsockname()[:2]
0938 except socket.error, x:
0939 if x.args[1] != "Bad file descriptor":
0940 raise
0941 else:
0942
0943
0944
0945
0946 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
0947 socket.SOCK_STREAM):
0948 af, socktype, proto, canonname, sa = res
0949 s = None
0950 try:
0951 s = socket.socket(af, socktype, proto)
0952
0953
0954 s.settimeout(1.0)
0955 s.connect((host, port))
0956 s.close()
0957 except socket.error:
0958 if s:
0959 s.close()
0960 if hasattr(sock, "close"):
0961 sock.close()
0962 self.socket = None
0963
0964
0965
0966 for worker in self._workerThreads:
0967 self.requests.put(_SHUTDOWNREQUEST)
0968
0969
0970 current = threading.currentThread()
0971 while self._workerThreads:
0972 worker = self._workerThreads.pop()
0973 if worker is not current and worker.isAlive:
0974 try:
0975 worker.join()
0976 except AssertionError:
0977 pass
0978
0979 def populate_ssl_environ(self):
0980 """Create WSGI environ entries to be merged into each request."""
0981 cert = open(self.ssl_certificate).read()
0982 cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
0983 self.ssl_environ = {
0984
0985
0986
0987
0988
0989 }
0990
0991
0992 self.ssl_environ.update({
0993 'SSL_SERVER_M_VERSION': cert.get_version(),
0994 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
0995
0996
0997 })
0998
0999 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