Package cherrypy :: Package wsgiserver
[hide private]
[frames] | no frames]

Source Code for Package cherrypy.wsgiserver

   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  # Not all of these names will be defined for every platform. 
  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  # de-dupe the list 
  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   
87 -class HTTPRequest(object):
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
106 - def __init__(self, connection):
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
120 - def parse_request(self):
121 """Parse the next HTTP request start-line and message-headers.""" 122 # HTTP/1.1 connections are persistent by default. If a client 123 # requests a page, then idles (leaves the connection open), 124 # then rfile.readline() will raise socket.error("timed out"). 125 # Note that it does this based on the value given to settimeout(), 126 # and doesn't need the client to request or acknowledge the close 127 # (although your TCP stack might suffer for it: cf Apache's history 128 # with FIN_WAIT_2). 129 request_line = self.rfile.readline() 130 if not request_line: 131 # Force self.ready = False so the connection will close. 132 self.ready = False 133 return 134 135 if request_line == "\r\n": 136 # RFC 2616 sec 4.1: "...if the server is reading the protocol 137 # stream at the beginning of a message and receives a CRLF 138 # first, it should ignore the CRLF." 139 # But only ignore one leading line! else we enable a DoS. 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 # path may be an abs_path (including "http://host.domain.tld"); 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 # Unquote the path+params (e.g. "/this%20path" -> "this path"). 166 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 167 # 168 # But note that "...a URI must be separated into its components 169 # before the escaped characters within those components can be 170 # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 171 atoms = [unquote(x) for x in quoted_slash.split(path)] 172 path = "%2F".join(atoms) 173 174 if path == "*": 175 # This means, of course, that the last wsgi_app (shortest path) 176 # will always handle a URI of "*". 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 # The mount_points list should be sorted by length, descending. 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 # Note that, like wsgiref and most other WSGI servers, 193 # we unquote the path but not the query string. 194 environ["QUERY_STRING"] = qs 195 196 # Compare request and server HTTP protocol versions, in case our 197 # server does not support the requested protocol. Limit our output 198 # to min(req, server). We want the following output: 199 # request server actual written supported response 200 # protocol protocol response protocol feature set 201 # a 1.0 1.0 1.0 1.0 202 # b 1.0 1.1 1.1 1.0 203 # c 1.1 1.0 1.0 1.0 204 # d 1.1 1.1 1.1 1.1 205 # Notice that, in (b), the response will be "HTTP/1.1" even though 206 # the client only understands 1.0. RFC 2616 10.5.6 says we should 207 # only return 505 if the _major_ version is different. 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 # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. 214 environ["SERVER_PROTOCOL"] = req_protocol 215 # set a non-standard environ entry so the WSGI app can know what 216 # the *real* server protocol is (and what features to support). 217 # See http://www.faqs.org/rfcs/rfc2145.html. 218 environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol 219 self.response_protocol = "HTTP/%s.%s" % min(rp, sp) 220 221 # If the Request-URI was an absoluteURI, use its location atom. 222 if location: 223 environ["SERVER_NAME"] = location 224 225 # then all the http headers 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 # Persistent connection support 239 if self.response_protocol == "HTTP/1.1": 240 if environ.get("HTTP_CONNECTION", "") == "close": 241 self.close_connection = True 242 else: 243 # HTTP/1.0 244 if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": 245 self.close_connection = True 246 247 # Transfer-Encoding support 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 # Note that, even if we see "chunked", we must reject 262 # if there is an extension we don't recognize. 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 # From PEP 333: 272 # "Servers and gateways that implement HTTP 1.1 must provide 273 # transparent support for HTTP 1.1's "expect/continue" mechanism. 274 # This may be done in any of several ways: 275 # 1. Respond to requests containing an Expect: 100-continue request 276 # with an immediate "100 Continue" response, and proceed normally. 277 # 2. Proceed with the request normally, but provide the application 278 # with a wsgi.input stream that will send the "100 Continue" 279 # response if/when the application first attempts to read from 280 # the input stream. The read request must then remain blocked 281 # until the client responds. 282 # 3. Wait until the client decides that the server does not support 283 # expect/continue, and sends the request body on its own. 284 # (This is suboptimal, and is not recommended.) 285 # 286 # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, 287 # but it seems like it would be a big slowdown for such a rare case. 288 if environ.get("HTTP_EXPECT", "") == "100-continue": 289 self.simple_response(100) 290 291 self.ready = True
292
293 - def read_headers(self):
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 # No more data--illegal end of headers 301 raise ValueError("Illegal end of headers.") 302 303 if line == '\r\n': 304 # Normal end of headers 305 break 306 307 if line[0] in ' \t': 308 # It's a continuation line. 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
328 - def decode_chunked(self):
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 ## if line: chunk_extension = line[0] 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 # Grab any trailer headers 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
355 - def respond(self):
356 """Call the appropriate WSGI app and write its iterable output.""" 357 response = self.wsgi_app(self.environ, self.start_response) 358 try: 359 for chunk in response: 360 # "The start_response callable must not actually transmit 361 # the response headers. Instead, it must store them for the 362 # server or gateway to transmit only after the first 363 # iteration of the application return value that yields 364 # a NON-EMPTY string, or upon the application's first 365 # invocation of the write() callable." (PEP 333) 366 if chunk: 367 self.write(chunk) 368 finally: 369 if hasattr(response, "close"): 370 response.close() 371 if (self.ready and not self.sent_headers 372 and not self.connection.server.interrupt): 373 self.sent_headers = True 374 self.send_headers() 375 if self.chunked_write: 376 self.sendall("0\r\n\r\n")
377
378 - def simple_response(self, status, msg=""):
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 # Request Entity Too Large 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
394 - def start_response(self, status, headers, exc_info = None):
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
410 - def write(self, chunk):
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
429 - def send_headers(self):
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 # Request Entity Too Large. Close conn to avoid garbage. 436 self.close_connection = True 437 elif "content-length" not in hkeys: 438 # "All 1xx (informational), 204 (no content), 439 # and 304 (not modified) responses MUST NOT 440 # include a message-body." So no point chunking. 441 if status < 200 or status in (204, 205, 304): 442 pass 443 else: 444 if self.response_protocol == 'HTTP/1.1': 445 # Use the chunked transfer-coding 446 self.chunked_write = True 447 self.outheaders.append(("Transfer-Encoding", "chunked")) 448 else: 449 # Closing the conn is the only way to determine len. 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
482 -class NoSSLError(Exception):
483 """Exception raised when a client speaks HTTP to an HTTPS socket.""" 484 pass
485 486
487 -def _ssl_wrap_method(method, is_reader=False):
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 ## print (id(self), method, args, kwargs) 495 start = time.time() 496 while True: 497 try: 498 return method(self, *args, **kwargs) 499 except (SSL.WantReadError, SSL.WantWriteError): 500 # Sleep and try again. This is dangerous, because it means 501 # the rest of the stack has no way of differentiating 502 # between a "new handshake" error and "client dropped". 503 # Note this isn't an endless loop: there's a timeout below. 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 # The client is talking HTTP to an HTTPS server. 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
533 -class SSL_fileobject(socket._fileobject):
534 """Faux file object attached to a socket object.""" 535 536 ssl_timeout = 3 537 ssl_retry = .01 538 539 close = _ssl_wrap_method(socket._fileobject.close) 540 flush = _ssl_wrap_method(socket._fileobject.flush) 541 write = _ssl_wrap_method(socket._fileobject.write) 542 writelines = _ssl_wrap_method(socket._fileobject.writelines) 543 read = _ssl_wrap_method(socket._fileobject.read, is_reader=True) 544 readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True) 545 readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
546 547
548 -class HTTPConnection(object):
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):
575 self.socket = sock 576 self.addr = addr 577 self.server = server 578 579 # Copy the class environ into self. 580 self.environ = self.environ.copy() 581 582 if SSL and isinstance(sock, SSL.ConnectionType): 583 timeout = sock.gettimeout() 584 self.rfile = SSL_fileobject(sock, "r", self.rbufsize) 585 self.rfile.ssl_timeout = timeout 586 self.sendall = _ssl_wrap_method(sock.sendall) 587 self.environ["wsgi.url_scheme"] = "https" 588 self.environ["HTTPS"] = "on" 589 sslenv = getattr(server, "ssl_environ", None) 590 if sslenv: 591 self.environ.update(sslenv) 592 else: 593 self.rfile = sock.makefile("rb", self.rbufsize) 594 self.sendall = sock.sendall 595 596 self.environ.update({"wsgi.input": self.rfile, 597 "SERVER_NAME": self.server.server_name, 598 }) 599 600 if isinstance(self.server.bind_addr, basestring): 601 # AF_UNIX. This isn't really allowed by WSGI, which doesn't 602 # address unix domain sockets. But it's better than nothing. 603 self.environ["SERVER_PORT"] = "" 604 else: 605 self.environ["SERVER_PORT"] = str(self.server.bind_addr[1]) 606 # optional values 607 # Until we do DNS lookups, omit REMOTE_HOST 608 self.environ["REMOTE_ADDR"] = self.addr[0] 609 self.environ["REMOTE_PORT"] = str(self.addr[1])
610
611 - def communicate(self):
612 """Read each request and respond appropriately.""" 613 try: 614 while True: 615 # (re)set req to None so that if something goes wrong in 616 # the RequestHandlerClass constructor, the error doesn't 617 # get written to the previous request. 618 req = None 619 req = self.RequestHandlerClass(self) 620 # This order of operations should guarantee correct pipelining. 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 # Unwrap our sendall 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
646 - def close(self):
647 """Close the socket underlying this connection.""" 648 self.rfile.close() 649 self.socket.close()
650 651
652 -def format_exc(limit=None):
653 """Like print_exc() but return a string. Backport for Python 2.3.""" 654 try: 655 etype, value, tb = sys.exc_info() 656 return ''.join(traceback.format_exception(etype, value, tb, limit)) 657 finally: 658 etype = value = tb = None
659 660 661 _SHUTDOWNREQUEST = None 662
663 -class WorkerThread(threading.Thread):
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
677 - def __init__(self, server):
678 self.ready = False 679 self.server = server 680 threading.Thread.__init__(self)
681
682 - def run(self):
683 try: 684 self.ready = True 685 while True: 686 conn = self.server.requests.get() 687 if conn is _SHUTDOWNREQUEST: 688 return 689 690 try: 691 conn.communicate() 692 finally: 693 conn.close() 694 except (KeyboardInterrupt, SystemExit), exc: 695 self.server.interrupt = exc
696 697
698 -class SSLConnection:
699 """A thread-safe wrapper for an SSL.Connection. 700 701 *args: the arguments to create the wrapped SSL.Connection(*args). 702 """ 703
704 - def __init__(self, *args):
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
725 -class CherryPyWSGIServer(object):
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 # Paths to certificate and private key files 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 # We've been handed a single wsgi_app, in CP-2.1 style. 774 # Assume it's mounted at "". 775 self.mount_points = [("", wsgi_app)] 776 else: 777 # We've been handed a list of (mount_point, wsgi_app) tuples, 778 # so that the server can call different wsgi_apps, and also 779 # correctly set SCRIPT_NAME. 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
794 - def start(self):
795 """Run the server forever.""" 796 # We don't have to trap KeyboardInterrupt or SystemExit here, 797 # because cherrpy.server already does so, calling self.stop() for us. 798 # If you're using this server with another framework, you should 799 # trap those exceptions in whatever code block calls start(). 800 self._interrupt = None 801 802 # Select the appropriate socket 803 if isinstance(self.bind_addr, basestring): 804 # AF_UNIX socket 805 806 # So we can reuse the socket... 807 try: os.unlink(self.bind_addr) 808 except: pass 809 810 # So everyone can access the socket... 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 # AF_INET or AF_INET6 socket 817 # Get the correct address family for our host (allows IPv6 addresses) 818 host, port = self.bind_addr 819 flags = 0 820 if host == '': 821 # Despite the socket module docs, using '' does not 822 # allow AI_PASSIVE to work. Passing None instead 823 # returns '0.0.0.0' like we want. In other words: 824 # host AI_PASSIVE result 825 # '' Y 192.168.x.y 826 # '' N 192.168.x.y 827 # None Y 0.0.0.0 828 # None N 127.0.0.1 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 # Probably a DNS issue. Assume IPv4. 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 # Timeout so KeyboardInterrupt can be caught on Win32 854 self.socket.settimeout(1) 855 self.socket.listen(self.request_queue_size) 856 857 # Create worker threads 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 # Wait for self.stop() to complete. See _set_interrupt. 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 ## self.socket.setsockopt(socket.SOL_SOCKET, socket.TCP_NODELAY, 1) 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 # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 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
893 - def tick(self):
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 # The only reason for the timeout in start() is so we can 905 # notice keyboard interrupts on Win32, which don't interrupt 906 # accept() by default 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 # Our socket was closed. 912 return 913 if msg == "Resource temporarily unavailable": 914 # Just try again. See http://www.cherrypy.org/ticket/479. 915 return 916 raise
917
918 - def _get_interrupt(self):
919 return self._interrupt
920 - def _set_interrupt(self, interrupt):
921 self._interrupt = True 922 self.stop() 923 self._interrupt = interrupt
924 interrupt = property(_get_interrupt, _set_interrupt, 925 doc="Set this to an Exception instance to " 926 "interrupt the server.") 927
928 - def stop(self):
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 # Touch our own socket to make accept() return immediately. 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 # Note that we're explicitly NOT using AI_PASSIVE, 943 # here, because we want an actual IP to touch. 944 # localhost won't work if we've bound to a public IP, 945 # but it would if we bound to INADDR_ANY via host = ''. 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 # See http://groups.google.com/group/cherrypy-users/ 953 # browse_frm/thread/bbfe5eb39c904fe0 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 # Must shut down threads here so the code that calls 965 # this method can know when all threads are stopped. 966 for worker in self._workerThreads: 967 self.requests.put(_SHUTDOWNREQUEST) 968 969 # Don't join currentThread (when stop is called inside a request). 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
979 - def populate_ssl_environ(self):
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 # pyOpenSSL doesn't provide access to any of these AFAICT 985 ## 'SSL_PROTOCOL': 'SSLv2', 986 ## SSL_CIPHER string The cipher specification name 987 ## SSL_VERSION_INTERFACE string The mod_ssl program version 988 ## SSL_VERSION_LIBRARY string The OpenSSL program version 989 } 990 991 # Server certificate attributes 992 self.ssl_environ.update({ 993 'SSL_SERVER_M_VERSION': cert.get_version(), 994 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), 995 ## 'SSL_SERVER_V_START': Validity of server's certificate (start time), 996 ## 'SSL_SERVER_V_END': Validity of server's certificate (end time), 997 }) 998 999 for prefix, dn in [("I", cert.get_issuer()), 1000 ("S", cert.get_subject())]: 1001 # X509Name objects don't seem to have a way to get the 1002 # complete DN string. Use str() and slice it instead, 1003 # because str(dn) == "<X509Name object '/C=US/ST=...'>" 1004 dnstr = str(dn)[18:-2] 1005 1006 wsgikey = 'SSL_SERVER_%s_DN' % prefix 1007 self.ssl_environ[wsgikey] = dnstr 1008 1009 # The DN should be of the form: /k1=v1/k2=v2, but we must allow 1010 # for any value to contain slashes itself (in a URL). 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