Skip to content
This repository
Browse code

Upgraded to CherryPy WSGIServer 3.2.0. (closes #66)

  • Loading branch information...
commit fa8151d38e5c959e9a2b606ec6ca3fb226a7bf76 1 parent cc76a03
Anand Chitipothu authored
12  web/httpserver.py
@@ -153,8 +153,16 @@ def WSGIServer(server_address, wsgi_app):
153 153
     """Creates CherryPy WSGI server listening at `server_address` to serve `wsgi_app`.
154 154
     This function can be overwritten to customize the webserver or use a different webserver.
155 155
     """
156  
-    from wsgiserver import CherryPyWSGIServer
157  
-    return CherryPyWSGIServer(server_address, wsgi_app, server_name="localhost")
  156
+    import wsgiserver
  157
+    
  158
+    # Default values of wsgiserver.ssl_adapters uses cheerypy.wsgiserver
  159
+    # prefix. Overwriting it make it work with web.wsgiserver.
  160
+    wsgiserver.ssl_adapters = {
  161
+        'builtin': 'web.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
  162
+        'pyopenssl': 'web.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
  163
+    }
  164
+    
  165
+    return wsgiserver.CherryPyWSGIServer(server_address, wsgi_app, server_name="localhost")
158 166
 
159 167
 class StaticApp(SimpleHTTPRequestHandler):
160 168
     """WSGI application for serving static files."""
1,813  web/wsgiserver/__init__.py
... ...
@@ -1,7 +1,7 @@
1  
-"""A high-speed, production ready, thread pooled, generic WSGI server.
  1
+"""A high-speed, production ready, thread pooled, generic HTTP server.
2 2
 
3 3
 Simplest example on how to use this module directly
4  
-(without using CherryPy's application machinery):
  4
+(without using CherryPy's application machinery)::
5 5
 
6 6
     from cherrypy import wsgiserver
7 7
     
@@ -9,37 +9,29 @@ def my_crazy_app(environ, start_response):
9 9
         status = '200 OK'
10 10
         response_headers = [('Content-type','text/plain')]
11 11
         start_response(status, response_headers)
12  
-        return ['Hello world!\n']
  12
+        return ['Hello world!']
13 13
     
14 14
     server = wsgiserver.CherryPyWSGIServer(
15 15
                 ('0.0.0.0', 8070), my_crazy_app,
16 16
                 server_name='www.cherrypy.example')
  17
+    server.start()
17 18
     
18 19
 The CherryPy WSGI server can serve as many WSGI applications 
19  
-as you want in one instance by using a WSGIPathInfoDispatcher:
  20
+as you want in one instance by using a WSGIPathInfoDispatcher::
20 21
     
21 22
     d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
22 23
     server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
23 24
     
24  
-Want SSL support? Just set these attributes:
25  
-    
26  
-    server.ssl_certificate = <filename>
27  
-    server.ssl_private_key = <filename>
28  
-    
29  
-    if __name__ == '__main__':
30  
-        try:
31  
-            server.start()
32  
-        except KeyboardInterrupt:
33  
-            server.stop()
  25
+Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance.
34 26
 
35 27
 This won't call the CherryPy engine (application side) at all, only the
36  
-WSGI server, which is independant from the rest of CherryPy. Don't
  28
+HTTP server, which is independent from the rest of CherryPy. Don't
37 29
 let the name "CherryPyWSGIServer" throw you; the name merely reflects
38 30
 its origin, not its coupling.
39 31
 
40 32
 For those of you wanting to understand internals of this module, here's the
41 33
 basic call flow. The server's listening thread runs a very tight loop,
42  
-sticking incoming connections onto a Queue:
  34
+sticking incoming connections onto a Queue::
43 35
 
44 36
     server = CherryPyWSGIServer(...)
45 37
     server.start()
@@ -52,7 +44,7 @@ def my_crazy_app(environ, start_response):
52 44
 
53 45
 Worker threads are kept in a pool and poll the Queue, popping off and then
54 46
 handling each connection in turn. Each connection can consist of an arbitrary
55  
-number of requests and their responses, so we run a nested loop:
  47
+number of requests and their responses, so we run a nested loop::
56 48
 
57 49
     while True:
58 50
         conn = server.requests.get()
@@ -62,9 +54,9 @@ def my_crazy_app(environ, start_response):
62 54
                 req.parse_request()
63 55
                 ->  # Read the Request-Line, e.g. "GET /page HTTP/1.1"
64 56
                     req.rfile.readline()
65  
-                    req.read_headers()
  57
+                    read_headers(req.rfile, req.inheaders)
66 58
                 req.respond()
67  
-                ->  response = wsgi_app(...)
  59
+                ->  response = app(...)
68 60
                     try:
69 61
                         for chunk in response:
70 62
                             if chunk:
@@ -76,35 +68,40 @@ def my_crazy_app(environ, start_response):
76 68
                     return
77 69
 """
78 70
 
79  
-
80  
-import base64
  71
+CRLF = '\r\n'
81 72
 import os
82 73
 import Queue
83 74
 import re
84 75
 quoted_slash = re.compile("(?i)%2F")
85 76
 import rfc822
86 77
 import socket
  78
+import sys
  79
+if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'):
  80
+    socket.IPPROTO_IPV6 = 41
87 81
 try:
88 82
     import cStringIO as StringIO
89 83
 except ImportError:
90 84
     import StringIO
  85
+DEFAULT_BUFFER_SIZE = -1
91 86
 
92 87
 _fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
93 88
 
94  
-import sys
95 89
 import threading
96 90
 import time
97 91
 import traceback
  92
+def format_exc(limit=None):
  93
+    """Like print_exc() but return a string. Backport for Python 2.3."""
  94
+    try:
  95
+        etype, value, tb = sys.exc_info()
  96
+        return ''.join(traceback.format_exception(etype, value, tb, limit))
  97
+    finally:
  98
+        etype = value = tb = None
  99
+
  100
+
98 101
 from urllib import unquote
99 102
 from urlparse import urlparse
100 103
 import warnings
101 104
 
102  
-try:
103  
-    from OpenSSL import SSL
104  
-    from OpenSSL import crypto
105  
-except ImportError:
106  
-    SSL = None
107  
-
108 105
 import errno
109 106
 
110 107
 def plat_specific_errors(*errnames):
@@ -133,51 +130,70 @@ def plat_specific_errors(*errnames):
133 130
     "EHOSTDOWN", "EHOSTUNREACH",
134 131
     )
135 132
 socket_errors_to_ignore.append("timed out")
  133
+socket_errors_to_ignore.append("The read operation timed out")
136 134
 
137 135
 socket_errors_nonblocking = plat_specific_errors(
138 136
     'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
139 137
 
140  
-comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
141  
-    'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
142  
-    'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
143  
-    'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
144  
-    'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
145  
-    'WWW-AUTHENTICATE']
  138
+comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
  139
+    'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control',
  140
+    'Connection', 'Content-Encoding', 'Content-Language', 'Expect',
  141
+    'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE',
  142
+    'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning',
  143
+    'WWW-Authenticate']
146 144
 
147 145
 
148  
-class WSGIPathInfoDispatcher(object):
149  
-    """A WSGI dispatcher for dispatch based on the PATH_INFO.
  146
+import logging
  147
+if not hasattr(logging, 'statistics'): logging.statistics = {}
  148
+
  149
+
  150
+def read_headers(rfile, hdict=None):
  151
+    """Read headers from the given stream into the given header dict.
150 152
     
151  
-    apps: a dict or list of (path_prefix, app) pairs.
  153
+    If hdict is None, a new header dict is created. Returns the populated
  154
+    header dict.
  155
+    
  156
+    Headers which are repeated are folded together using a comma if their
  157
+    specification so dictates.
  158
+    
  159
+    This function raises ValueError when the read bytes violate the HTTP spec.
  160
+    You should probably return "400 Bad Request" if this happens.
152 161
     """
  162
+    if hdict is None:
  163
+        hdict = {}
153 164
     
154  
-    def __init__(self, apps):
155  
-        try:
156  
-            apps = apps.items()
157  
-        except AttributeError:
158  
-            pass
  165
+    while True:
  166
+        line = rfile.readline()
  167
+        if not line:
  168
+            # No more data--illegal end of headers
  169
+            raise ValueError("Illegal end of headers.")
159 170
         
160  
-        # Sort the apps by len(path), descending
161  
-        apps.sort()
162  
-        apps.reverse()
  171
+        if line == CRLF:
  172
+            # Normal end of headers
  173
+            break
  174
+        if not line.endswith(CRLF):
  175
+            raise ValueError("HTTP requires CRLF terminators")
163 176
         
164  
-        # The path_prefix strings must start, but not end, with a slash.
165  
-        # Use "" instead of "/".
166  
-        self.apps = [(p.rstrip("/"), a) for p, a in apps]
167  
-    
168  
-    def __call__(self, environ, start_response):
169  
-        path = environ["PATH_INFO"] or "/"
170  
-        for p, app in self.apps:
171  
-            # The apps list should be sorted by length, descending.
172  
-            if path.startswith(p + "/") or path == p:
173  
-                environ = environ.copy()
174  
-                environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
175  
-                environ["PATH_INFO"] = path[len(p):]
176  
-                return app(environ, start_response)
  177
+        if line[0] in ' \t':
  178
+            # It's a continuation line.
  179
+            v = line.strip()
  180
+        else:
  181
+            try:
  182
+                k, v = line.split(":", 1)
  183
+            except ValueError:
  184
+                raise ValueError("Illegal header line.")
  185
+            # TODO: what about TE and WWW-Authenticate?
  186
+            k = k.strip().title()
  187
+            v = v.strip()
  188
+            hname = k
177 189
         
178  
-        start_response('404 Not Found', [('Content-Type', 'text/plain'),
179  
-                                         ('Content-Length', '0')])
180  
-        return ['']
  190
+        if k in comma_separated_headers:
  191
+            existing = hdict.get(hname)
  192
+            if existing:
  193
+                v = ", ".join((existing, v))
  194
+        hdict[hname] = v
  195
+    
  196
+    return hdict
181 197
 
182 198
 
183 199
 class MaxSizeExceeded(Exception):
@@ -246,70 +262,293 @@ def next(self):
246 262
         return data
247 263
 
248 264
 
  265
+class KnownLengthRFile(object):
  266
+    """Wraps a file-like object, returning an empty string when exhausted."""
  267
+    
  268
+    def __init__(self, rfile, content_length):
  269
+        self.rfile = rfile
  270
+        self.remaining = content_length
  271
+    
  272
+    def read(self, size=None):
  273
+        if self.remaining == 0:
  274
+            return ''
  275
+        if size is None:
  276
+            size = self.remaining
  277
+        else:
  278
+            size = min(size, self.remaining)
  279
+        
  280
+        data = self.rfile.read(size)
  281
+        self.remaining -= len(data)
  282
+        return data
  283
+    
  284
+    def readline(self, size=None):
  285
+        if self.remaining == 0:
  286
+            return ''
  287
+        if size is None:
  288
+            size = self.remaining
  289
+        else:
  290
+            size = min(size, self.remaining)
  291
+        
  292
+        data = self.rfile.readline(size)
  293
+        self.remaining -= len(data)
  294
+        return data
  295
+    
  296
+    def readlines(self, sizehint=0):
  297
+        # Shamelessly stolen from StringIO
  298
+        total = 0
  299
+        lines = []
  300
+        line = self.readline(sizehint)
  301
+        while line:
  302
+            lines.append(line)
  303
+            total += len(line)
  304
+            if 0 < sizehint <= total:
  305
+                break
  306
+            line = self.readline(sizehint)
  307
+        return lines
  308
+    
  309
+    def close(self):
  310
+        self.rfile.close()
  311
+    
  312
+    def __iter__(self):
  313
+        return self
  314
+    
  315
+    def __next__(self):
  316
+        data = next(self.rfile)
  317
+        self.remaining -= len(data)
  318
+        return data
  319
+
  320
+
  321
+class ChunkedRFile(object):
  322
+    """Wraps a file-like object, returning an empty string when exhausted.
  323
+    
  324
+    This class is intended to provide a conforming wsgi.input value for
  325
+    request entities that have been encoded with the 'chunked' transfer
  326
+    encoding.
  327
+    """
  328
+    
  329
+    def __init__(self, rfile, maxlen, bufsize=8192):
  330
+        self.rfile = rfile
  331
+        self.maxlen = maxlen
  332
+        self.bytes_read = 0
  333
+        self.buffer = ''
  334
+        self.bufsize = bufsize
  335
+        self.closed = False
  336
+    
  337
+    def _fetch(self):
  338
+        if self.closed:
  339
+            return
  340
+        
  341
+        line = self.rfile.readline()
  342
+        self.bytes_read += len(line)
  343
+        
  344
+        if self.maxlen and self.bytes_read > self.maxlen:
  345
+            raise MaxSizeExceeded("Request Entity Too Large", self.maxlen)
  346
+        
  347
+        line = line.strip().split(";", 1)
  348
+        
  349
+        try:
  350
+            chunk_size = line.pop(0)
  351
+            chunk_size = int(chunk_size, 16)
  352
+        except ValueError:
  353
+            raise ValueError("Bad chunked transfer size: " + repr(chunk_size))
  354
+        
  355
+        if chunk_size <= 0:
  356
+            self.closed = True
  357
+            return
  358
+        
  359
+##            if line: chunk_extension = line[0]
  360
+        
  361
+        if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
  362
+            raise IOError("Request Entity Too Large")
  363
+        
  364
+        chunk = self.rfile.read(chunk_size)
  365
+        self.bytes_read += len(chunk)
  366
+        self.buffer += chunk
  367
+        
  368
+        crlf = self.rfile.read(2)
  369
+        if crlf != CRLF:
  370
+            raise ValueError(
  371
+                 "Bad chunked transfer coding (expected '\\r\\n', "
  372
+                 "got " + repr(crlf) + ")")
  373
+    
  374
+    def read(self, size=None):
  375
+        data = ''
  376
+        while True:
  377
+            if size and len(data) >= size:
  378
+                return data
  379
+            
  380
+            if not self.buffer:
  381
+                self._fetch()
  382
+                if not self.buffer:
  383
+                    # EOF
  384
+                    return data
  385
+            
  386
+            if size:
  387
+                remaining = size - len(data)
  388
+                data += self.buffer[:remaining]
  389
+                self.buffer = self.buffer[remaining:]
  390
+            else:
  391
+                data += self.buffer
  392
+    
  393
+    def readline(self, size=None):
  394
+        data = ''
  395
+        while True:
  396
+            if size and len(data) >= size:
  397
+                return data
  398
+            
  399
+            if not self.buffer:
  400
+                self._fetch()
  401
+                if not self.buffer:
  402
+                    # EOF
  403
+                    return data
  404
+            
  405
+            newline_pos = self.buffer.find('\n')
  406
+            if size:
  407
+                if newline_pos == -1:
  408
+                    remaining = size - len(data)
  409
+                    data += self.buffer[:remaining]
  410
+                    self.buffer = self.buffer[remaining:]
  411
+                else:
  412
+                    remaining = min(size - len(data), newline_pos)
  413
+                    data += self.buffer[:remaining]
  414
+                    self.buffer = self.buffer[remaining:]
  415
+            else:
  416
+                if newline_pos == -1:
  417
+                    data += self.buffer
  418
+                else:
  419
+                    data += self.buffer[:newline_pos]
  420
+                    self.buffer = self.buffer[newline_pos:]
  421
+    
  422
+    def readlines(self, sizehint=0):
  423
+        # Shamelessly stolen from StringIO
  424
+        total = 0
  425
+        lines = []
  426
+        line = self.readline(sizehint)
  427
+        while line:
  428
+            lines.append(line)
  429
+            total += len(line)
  430
+            if 0 < sizehint <= total:
  431
+                break
  432
+            line = self.readline(sizehint)
  433
+        return lines
  434
+    
  435
+    def read_trailer_lines(self):
  436
+        if not self.closed:
  437
+            raise ValueError(
  438
+                "Cannot read trailers until the request body has been read.")
  439
+        
  440
+        while True:
  441
+            line = self.rfile.readline()
  442
+            if not line:
  443
+                # No more data--illegal end of headers
  444
+                raise ValueError("Illegal end of headers.")
  445
+            
  446
+            self.bytes_read += len(line)
  447
+            if self.maxlen and self.bytes_read > self.maxlen:
  448
+                raise IOError("Request Entity Too Large")
  449
+            
  450
+            if line == CRLF:
  451
+                # Normal end of headers
  452
+                break
  453
+            if not line.endswith(CRLF):
  454
+                raise ValueError("HTTP requires CRLF terminators")
  455
+            
  456
+            yield line
  457
+    
  458
+    def close(self):
  459
+        self.rfile.close()
  460
+    
  461
+    def __iter__(self):
  462
+        # Shamelessly stolen from StringIO
  463
+        total = 0
  464
+        line = self.readline(sizehint)
  465
+        while line:
  466
+            yield line
  467
+            total += len(line)
  468
+            if 0 < sizehint <= total:
  469
+                break
  470
+            line = self.readline(sizehint)
  471
+
  472
+
249 473
 class HTTPRequest(object):
250 474
     """An HTTP Request (and response).
251 475
     
252 476
     A single HTTP connection may consist of multiple request/response pairs.
253  
-    
254  
-    send: the 'send' method from the connection's socket object.
255  
-    wsgi_app: the WSGI application to call.
256  
-    environ: a partial WSGI environ (server and connection entries).
257  
-        The caller MUST set the following entries:
258  
-        * All wsgi.* entries, including .input
259  
-        * SERVER_NAME and SERVER_PORT
260  
-        * Any SSL_* entries
261  
-        * Any custom entries like REMOTE_ADDR and REMOTE_PORT
262  
-        * SERVER_SOFTWARE: the value to write in the "Server" response header.
263  
-        * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of
264  
-            the response. From RFC 2145: "An HTTP server SHOULD send a
265  
-            response version equal to the highest version for which the
266  
-            server is at least conditionally compliant, and whose major
267  
-            version is less than or equal to the one received in the
268  
-            request.  An HTTP server MUST NOT send a version for which
269  
-            it is not at least conditionally compliant."
270  
-    
271  
-    outheaders: a list of header tuples to write in the response.
272  
-    ready: when True, the request has been parsed and is ready to begin
273  
-        generating the response. When False, signals the calling Connection
274  
-        that the response should not be generated and the connection should
275  
-        close.
276  
-    close_connection: signals the calling Connection that the request
277  
-        should close. This does not imply an error! The client and/or
278  
-        server may each request that the connection be closed.
279  
-    chunked_write: if True, output will be encoded with the "chunked"
280  
-        transfer-coding. This value is set automatically inside
281  
-        send_headers.
282 477
     """
283 478
     
284  
-    max_request_header_size = 0
285  
-    max_request_body_size = 0
  479
+    server = None
  480
+    """The HTTPServer object which is receiving this request."""
286 481
     
287  
-    def __init__(self, wfile, environ, wsgi_app):
288  
-        self.rfile = environ['wsgi.input']
289  
-        self.wfile = wfile
290  
-        self.environ = environ.copy()
291  
-        self.wsgi_app = wsgi_app
  482
+    conn = None
  483
+    """The HTTPConnection object on which this request connected."""
  484
+    
  485
+    inheaders = {}
  486
+    """A dict of request headers."""
  487
+    
  488
+    outheaders = []
  489
+    """A list of header tuples to write in the response."""
  490
+    
  491
+    ready = False
  492
+    """When True, the request has been parsed and is ready to begin generating
  493
+    the response. When False, signals the calling Connection that the response
  494
+    should not be generated and the connection should close."""
  495
+    
  496
+    close_connection = False
  497
+    """Signals the calling Connection that the request should close. This does
  498
+    not imply an error! The client and/or server may each request that the
  499
+    connection be closed."""
  500
+    
  501
+    chunked_write = False
  502
+    """If True, output will be encoded with the "chunked" transfer-coding.
  503
+    
  504
+    This value is set automatically inside send_headers."""
  505
+    
  506
+    def __init__(self, server, conn):
  507
+        self.server= server
  508
+        self.conn = conn
292 509
         
293 510
         self.ready = False
294  
-        self.started_response = False
  511
+        self.started_request = False
  512
+        self.scheme = "http"
  513
+        if self.server.ssl_adapter is not None:
  514
+            self.scheme = "https"
  515
+        # Use the lowest-common protocol in case read_request_line errors.
  516
+        self.response_protocol = 'HTTP/1.0'
  517
+        self.inheaders = {}
  518
+        
295 519
         self.status = ""
296 520
         self.outheaders = []
297 521
         self.sent_headers = False
298  
-        self.close_connection = False
299  
-        self.chunked_write = False
  522
+        self.close_connection = self.__class__.close_connection
  523
+        self.chunked_read = False
  524
+        self.chunked_write = self.__class__.chunked_write
300 525
     
301 526
     def parse_request(self):
302 527
         """Parse the next HTTP request start-line and message-headers."""
303  
-        self.rfile.maxlen = self.max_request_header_size
304  
-        self.rfile.bytes_read = 0
  528
+        self.rfile = SizeCheckWrapper(self.conn.rfile,
  529
+                                      self.server.max_request_header_size)
  530
+        try:
  531
+            self.read_request_line()
  532
+        except MaxSizeExceeded:
  533
+            self.simple_response("414 Request-URI Too Long",
  534
+                "The Request-URI sent with the request exceeds the maximum "
  535
+                "allowed bytes.")
  536
+            return
305 537
         
306 538
         try:
307  
-            self._parse_request()
  539
+            success = self.read_request_headers()
308 540
         except MaxSizeExceeded:
309  
-            self.simple_response("413 Request Entity Too Large")
  541
+            self.simple_response("413 Request Entity Too Large",
  542
+                "The headers sent with the request exceed the maximum "
  543
+                "allowed bytes.")
310 544
             return
  545
+        else:
  546
+            if not success:
  547
+                return
  548
+        
  549
+        self.ready = True
311 550
     
312  
-    def _parse_request(self):
  551
+    def read_request_line(self):
313 552
         # HTTP/1.1 connections are persistent by default. If a client
314 553
         # requests a page, then idles (leaves the connection open),
315 554
         # then rfile.readline() will raise socket.error("timed out").
@@ -318,12 +557,16 @@ def _parse_request(self):
318 557
         # (although your TCP stack might suffer for it: cf Apache's history
319 558
         # with FIN_WAIT_2).
320 559
         request_line = self.rfile.readline()
  560
+        
  561
+        # Set started_request to True so communicate() knows to send 408
  562
+        # from here on out.
  563
+        self.started_request = True
321 564
         if not request_line:
322 565
             # Force self.ready = False so the connection will close.
323 566
             self.ready = False
324 567
             return
325 568
         
326  
-        if request_line == "\r\n":
  569
+        if request_line == CRLF:
327 570
             # RFC 2616 sec 4.1: "...if the server is reading the protocol
328 571
             # stream at the beginning of a message and receives a CRLF
329 572
             # first, it should ignore the CRLF."
@@ -333,44 +576,52 @@ def _parse_request(self):
333 576
                 self.ready = False
334 577
                 return
335 578
         
336  
-        environ = self.environ
  579
+        if not request_line.endswith(CRLF):
  580
+            self.simple_response("400 Bad Request", "HTTP requires CRLF terminators")
  581
+            return
337 582
         
338 583
         try:
339  
-            method, path, req_protocol = request_line.strip().split(" ", 2)
340  
-        except ValueError:
341  
-            self.simple_response(400, "Malformed Request-Line")
  584
+            method, uri, req_protocol = request_line.strip().split(" ", 2)
  585
+            rp = int(req_protocol[5]), int(req_protocol[7])
  586
+        except (ValueError, IndexError):
  587
+            self.simple_response("400 Bad Request", "Malformed Request-Line")
342 588
             return
343 589
         
344  
-        environ["REQUEST_METHOD"] = method
345  
-        
346  
-        # path may be an abs_path (including "http://host.domain.tld");
347  
-        scheme, location, path, params, qs, frag = urlparse(path)
  590
+        self.uri = uri
  591
+        self.method = method
348 592
         
349  
-        if frag:
  593
+        # uri may be an abs_path (including "http://host.domain.tld");
  594
+        scheme, authority, path = self.parse_request_uri(uri)
  595
+        if '#' in path:
350 596
             self.simple_response("400 Bad Request",
351 597
                                  "Illegal #fragment in Request-URI.")
352 598
             return
353 599
         
354 600
         if scheme:
355  
-            environ["wsgi.url_scheme"] = scheme
356  
-        if params:
357  
-            path = path + ";" + params
  601
+            self.scheme = scheme
358 602
         
359  
-        environ["SCRIPT_NAME"] = ""
  603
+        qs = ''
  604
+        if '?' in path:
  605
+            path, qs = path.split('?', 1)
360 606
         
361  
-        # Unquote the path+params (e.g. "/this%20path" -> "this path").
  607
+        # Unquote the path+params (e.g. "/this%20path" -> "/this path").
362 608
         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
363 609
         #
364 610
         # But note that "...a URI must be separated into its components
365 611
         # before the escaped characters within those components can be
366 612
         # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
367  
-        atoms = [unquote(x) for x in quoted_slash.split(path)]
  613
+        # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path".
  614
+        try:
  615
+            atoms = [unquote(x) for x in quoted_slash.split(path)]
  616
+        except ValueError, ex:
  617
+            self.simple_response("400 Bad Request", ex.args[0])
  618
+            return
368 619
         path = "%2F".join(atoms)
369  
-        environ["PATH_INFO"] = path
  620
+        self.path = path
370 621
         
371  
-        # Note that, like wsgiref and most other WSGI servers,
372  
-        # we unquote the path but not the query string.
373  
-        environ["QUERY_STRING"] = qs
  622
+        # Note that, like wsgiref and most other HTTP servers,
  623
+        # we "% HEX HEX"-unquote the path but not the query string.
  624
+        self.qs = qs
374 625
         
375 626
         # Compare request and server HTTP protocol versions, in case our
376 627
         # server does not support the requested protocol. Limit our output
@@ -384,46 +635,45 @@ def _parse_request(self):
384 635
         # Notice that, in (b), the response will be "HTTP/1.1" even though
385 636
         # the client only understands 1.0. RFC 2616 10.5.6 says we should
386 637
         # only return 505 if the _major_ version is different.
387  
-        rp = int(req_protocol[5]), int(req_protocol[7])
388  
-        server_protocol = environ["ACTUAL_SERVER_PROTOCOL"]
389  
-        sp = int(server_protocol[5]), int(server_protocol[7])
  638
+        sp = int(self.server.protocol[5]), int(self.server.protocol[7])
  639
+        
390 640
         if sp[0] != rp[0]:
391 641
             self.simple_response("505 HTTP Version Not Supported")
392 642
             return
393  
-        # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
394  
-        environ["SERVER_PROTOCOL"] = req_protocol
  643
+        self.request_protocol = req_protocol
395 644
         self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
396  
-        
397  
-        # If the Request-URI was an absoluteURI, use its location atom.
398  
-        if location:
399  
-            environ["SERVER_NAME"] = location
  645
+    
  646
+    def read_request_headers(self):
  647
+        """Read self.rfile into self.inheaders. Return success."""
400 648
         
401 649
         # then all the http headers
402 650
         try:
403  
-            self.read_headers()
  651
+            read_headers(self.rfile, self.inheaders)
404 652
         except ValueError, ex:
405  
-            self.simple_response("400 Bad Request", repr(ex.args))
406  
-            return
  653
+            self.simple_response("400 Bad Request", ex.args[0])
  654
+            return False
407 655
         
408  
-        mrbs = self.max_request_body_size
409  
-        if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs:
410  
-            self.simple_response("413 Request Entity Too Large")
411  
-            return
  656
+        mrbs = self.server.max_request_body_size
  657
+        if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs:
  658
+            self.simple_response("413 Request Entity Too Large",
  659
+                "The entity sent with the request exceeds the maximum "
  660
+                "allowed bytes.")
  661
+            return False
412 662
         
413 663
         # Persistent connection support
414 664
         if self.response_protocol == "HTTP/1.1":
415 665
             # Both server and client are HTTP/1.1
416  
-            if environ.get("HTTP_CONNECTION", "") == "close":
  666
+            if self.inheaders.get("Connection", "") == "close":
417 667
                 self.close_connection = True
418 668
         else:
419 669
             # Either the server or client (or both) are HTTP/1.0
420  
-            if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
  670
+            if self.inheaders.get("Connection", "") != "Keep-Alive":
421 671
                 self.close_connection = True
422 672
         
423 673
         # Transfer-Encoding support
424 674
         te = None
425 675
         if self.response_protocol == "HTTP/1.1":
426  
-            te = environ.get("HTTP_TRANSFER_ENCODING")
  676
+            te = self.inheaders.get("Transfer-Encoding")
427 677
             if te:
428 678
                 te = [x.strip().lower() for x in te.split(",") if x.strip()]
429 679
         
@@ -438,7 +688,7 @@ def _parse_request(self):
438 688
                     # if there is an extension we don't recognize.
439 689
                     self.simple_response("501 Unimplemented")
440 690
                     self.close_connection = True
441  
-                    return
  691
+                    return False
442 692
         
443 693
         # From PEP 333:
444 694
         # "Servers and gateways that implement HTTP 1.1 must provide
@@ -457,188 +707,125 @@ def _parse_request(self):
457 707
         #
458 708
         # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
459 709
         # but it seems like it would be a big slowdown for such a rare case.
460  
-        if environ.get("HTTP_EXPECT", "") == "100-continue":
461  
-            self.simple_response(100)
462  
-        
463  
-        self.ready = True
  710
+        if self.inheaders.get("Expect", "") == "100-continue":
  711
+            # Don't use simple_response here, because it emits headers
  712
+            # we don't want. See http://www.cherrypy.org/ticket/951
  713
+            msg = self.server.protocol + " 100 Continue\r\n\r\n"
  714
+            try:
  715
+                self.conn.wfile.sendall(msg)
  716
+            except socket.error, x:
  717
+                if x.args[0] not in socket_errors_to_ignore:
  718
+                    raise
  719
+        return True
464 720
     
465  
-    def read_headers(self):
466  
-        """Read header lines from the incoming stream."""
467  
-        environ = self.environ
  721
+    def parse_request_uri(self, uri):
  722
+        """Parse a Request-URI into (scheme, authority, path).
468 723
         
469  
-        while True:
470  
-            line = self.rfile.readline()
471  
-            if not line:
472  
-                # No more data--illegal end of headers
473  
-                raise ValueError("Illegal end of headers.")
474  
-            
475  
-            if line == '\r\n':
476  
-                # Normal end of headers
477  
-                break
478  
-            
479  
-            if line[0] in ' \t':
480  
-                # It's a continuation line.
481  
-                v = line.strip()
482  
-            else:
483  
-                k, v = line.split(":", 1)
484  
-                k, v = k.strip().upper(), v.strip()
485  
-                envname = "HTTP_" + k.replace("-", "_")
  724
+        Note that Request-URI's must be one of::
486 725
             
487  
-            if k in comma_separated_headers:
488  
-                existing = environ.get(envname)
489  
-                if existing:
490  
-                    v = ", ".join((existing, v))
491  
-            environ[envname] = v
  726
+            Request-URI    = "*" | absoluteURI | abs_path | authority
492 727
         
493  
-        ct = environ.pop("HTTP_CONTENT_TYPE", None)
494  
-        if ct is not None:
495  
-            environ["CONTENT_TYPE"] = ct
496  
-        cl = environ.pop("HTTP_CONTENT_LENGTH", None)
497  
-        if cl is not None:
498  
-            environ["CONTENT_LENGTH"] = cl
499  
-    
500  
-    def decode_chunked(self):
501  
-        """Decode the 'chunked' transfer coding."""
502  
-        cl = 0
503  
-        data = StringIO.StringIO()
504  
-        while True:
505  
-            line = self.rfile.readline().strip().split(";", 1)
506  
-            chunk_size = int(line.pop(0), 16)
507  
-            if chunk_size <= 0:
508  
-                break
509  
-##            if line: chunk_extension = line[0]
510  
-            cl += chunk_size
511  
-            data.write(self.rfile.read(chunk_size))
512  
-            crlf = self.rfile.read(2)
513  
-            if crlf != "\r\n":
514  
-                self.simple_response("400 Bad Request",
515  
-                                     "Bad chunked transfer coding "
516  
-                                     "(expected '\\r\\n', got %r)" % crlf)
517  
-                return
  728
+        Therefore, a Request-URI which starts with a double forward-slash
  729
+        cannot be a "net_path"::
518 730
         
519  
-        # Grab any trailer headers
520  
-        self.read_headers()
  731
+            net_path      = "//" authority [ abs_path ]
521 732
         
522  
-        data.seek(0)
523  
-        self.environ["wsgi.input"] = data
524  
-        self.environ["CONTENT_LENGTH"] = str(cl) or ""
525  
-        return True
  733
+        Instead, it must be interpreted as an "abs_path" with an empty first
  734
+        path segment::
  735
+        
  736
+            abs_path      = "/"  path_segments
  737
+            path_segments = segment *( "/" segment )
  738
+            segment       = *pchar *( ";" param )
  739
+            param         = *pchar
  740
+        """
  741
+        if uri == "*":
  742
+            return None, None, uri
  743
+        
  744
+        i = uri.find('://')
  745
+        if i > 0 and '?' not in uri[:i]:
  746
+            # An absoluteURI.
  747
+            # If there's a scheme (and it must be http or https), then:
  748
+            # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
  749
+            scheme, remainder = uri[:i].lower(), uri[i + 3:]
  750
+            authority, path = remainder.split("/", 1)
  751
+            return scheme, authority, path
  752
+        
  753
+        if uri.startswith('/'):
  754
+            # An abs_path.
  755
+            return None, None, uri
  756
+        else:
  757
+            # An authority.
  758
+            return None, uri, None
526 759
     
527 760
     def respond(self):
528  
-        """Call the appropriate WSGI app and write its iterable output."""
529  
-        # Set rfile.maxlen to ensure we don't read past Content-Length.
530  
-        # This will also be used to read the entire request body if errors
531  
-        # are raised before the app can read the body.
  761
+        """Call the gateway and write its iterable output."""
  762
+        mrbs = self.server.max_request_body_size
532 763
         if self.chunked_read:
533  
-            # If chunked, Content-Length will be 0.
534  
-            self.rfile.maxlen = self.max_request_body_size
  764
+            self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
535 765
         else:
536  
-            cl = int(self.environ.get("CONTENT_LENGTH", 0))
537  
-            if self.max_request_body_size:
538  
-                self.rfile.maxlen = min(cl, self.max_request_body_size)
539  
-            else:
540  
-                self.rfile.maxlen = cl
541  
-        self.rfile.bytes_read = 0
542  
-        
543  
-        try:
544  
-            self._respond()
545  
-        except MaxSizeExceeded:
546  
-            if not self.sent_headers:
547  
-                self.simple_response("413 Request Entity Too Large")
548  
-            return
549  
-    
550  
-    def _respond(self):
551  
-        if self.chunked_read:
552  
-            if not self.decode_chunked():
553  
-                self.close_connection = True
  766
+            cl = int(self.inheaders.get("Content-Length", 0))
  767
+            if mrbs and mrbs < cl:
  768
+                if not self.sent_headers:
  769
+                    self.simple_response("413 Request Entity Too Large",
  770
+                        "The entity sent with the request exceeds the maximum "
  771
+                        "allowed bytes.")
554 772
                 return
  773
+            self.rfile = KnownLengthRFile(self.conn.rfile, cl)
555 774
         
556  
-        response = self.wsgi_app(self.environ, self.start_response)
557  
-        try:
558  
-            for chunk in response:
559  
-                # "The start_response callable must not actually transmit
560  
-                # the response headers. Instead, it must store them for the
561  
-                # server or gateway to transmit only after the first
562  
-                # iteration of the application return value that yields
563  
-                # a NON-EMPTY string, or upon the application's first
564  
-                # invocation of the write() callable." (PEP 333)
565  
-                if chunk:
566  
-                    self.write(chunk)
567  
-        finally:
568  
-            if hasattr(response, "close"):
569  
-                response.close()
  775
+        self.server.gateway(self).respond()
570 776
         
571 777
         if (self.ready and not self.sent_headers):
572 778
             self.sent_headers = True
573 779
             self.send_headers()
574 780
         if self.chunked_write:
575  
-            self.wfile.sendall("0\r\n\r\n")
  781
+            self.conn.wfile.sendall("0\r\n\r\n")
576 782
     
577 783
     def simple_response(self, status, msg=""):
578 784
         """Write a simple response back to the client."""
579 785
         status = str(status)
580  
-        buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status),
  786
+        buf = [self.server.protocol + " " +
  787
+               status + CRLF,
581 788
                "Content-Length: %s\r\n" % len(msg),
582 789
                "Content-Type: text/plain\r\n"]
583 790
         
584  
-        if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
585  
-            # Request Entity Too Large
  791
+        if status[:3] in ("413", "414"):
  792
+            # Request Entity Too Large / Request-URI Too Long
586 793
             self.close_connection = True
587  
-            buf.append("Connection: close\r\n")
  794
+            if self.response_protocol == 'HTTP/1.1':
  795
+                # This will not be true for 414, since read_request_line
  796
+                # usually raises 414 before reading the whole line, and we
  797
+                # therefore cannot know the proper response_protocol.
  798
+                buf.append("Connection: close\r\n")
  799
+            else:
  800
+                # HTTP/1.0 had no 413/414 status nor Connection header.
  801
+                # Emit 400 instead and trust the message body is enough.
  802
+                status = "400 Bad Request"
588 803
         
589  
-        buf.append("\r\n")
  804
+        buf.append(CRLF)
590 805
         if msg:
  806
+            if isinstance(msg, unicode):
  807
+                msg = msg.encode("ISO-8859-1")
591 808
             buf.append(msg)
592 809
         
593 810
         try:
594  
-            self.wfile.sendall("".join(buf))
  811
+            self.conn.wfile.sendall("".join(buf))
595 812
         except socket.error, x:
596 813
             if x.args[0] not in socket_errors_to_ignore:
597 814
                 raise
598 815
     
599  
-    def start_response(self, status, headers, exc_info = None):
600  
-        """WSGI callable to begin the HTTP response."""
601  
-        # "The application may call start_response more than once,
602  
-        # if and only if the exc_info argument is provided."
603  
-        if self.started_response and not exc_info:
604  
-            raise AssertionError("WSGI start_response called a second "
605  
-                                 "time with no exc_info.")
606  
-        
607  
-        # "if exc_info is provided, and the HTTP headers have already been
608  
-        # sent, start_response must raise an error, and should raise the
609  
-        # exc_info tuple."
610  
-        if self.sent_headers:
611  
-            try:
612  
-                raise exc_info[0], exc_info[1], exc_info[2]
613  
-            finally:
614  
-                exc_info = None
615  
-        
616  
-        self.started_response = True
617  
-        self.status = status
618  
-        self.outheaders.extend(headers)
619  
-        return self.write
620  
-    
621 816
     def write(self, chunk):
622  
-        """WSGI callable to write unbuffered data to the client.
623  
-        
624  
-        This method is also used internally by start_response (to write
625  
-        data from the iterable returned by the WSGI application).
626  
-        """
627  
-        if not self.started_response:
628  
-            raise AssertionError("WSGI write called before start_response.")
629  
-        
630  
-        if not self.sent_headers:
631  
-            self.sent_headers = True
632  
-            self.send_headers()
633  
-        
  817
+        """Write unbuffered data to the client."""
634 818
         if self.chunked_write and chunk:
635  
-            buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
636  
-            self.wfile.sendall("".join(buf))
  819
+            buf = [hex(len(chunk))[2:], CRLF, chunk, CRLF]
  820
+            self.conn.wfile.sendall("".join(buf))
637 821
         else:
638  
-            self.wfile.sendall(chunk)
  822
+            self.conn.wfile.sendall(chunk)
639 823
     
640 824
     def send_headers(self):
641  
-        """Assert, process, and send the HTTP response message-headers."""
  825
+        """Assert, process, and send the HTTP response message-headers.
  826
+        
  827
+        You must set self.status, and self.outheaders before calling this.
  828
+        """
642 829
         hkeys = [key.lower() for key, value in self.outheaders]
643 830
         status = int(self.status[:3])
644 831
         
@@ -653,7 +840,7 @@ def send_headers(self):
653 840
                 pass
654 841
             else:
655 842
                 if (self.response_protocol == 'HTTP/1.1'
656  
-                    and self.environ["REQUEST_METHOD"] != 'HEAD'):
  843
+                    and self.method != 'HEAD'):
657 844
                     # Use the chunked transfer-coding
658 845
                     self.chunked_write = True
659 846
                     self.outheaders.append(("Transfer-Encoding", "chunked"))
@@ -684,28 +871,21 @@ def send_headers(self):
684 871
             # requirement is not be construed as preventing a server from
685 872
             # defending itself against denial-of-service attacks, or from
686 873
             # badly broken client implementations."
687  
-            size = self.rfile.maxlen - self.rfile.bytes_read
688  
-            if size > 0:
689  
-                self.rfile.read(size)
  874
+            remaining = getattr(self.rfile, 'remaining', 0)
  875
+            if remaining > 0:
  876
+                self.rfile.read(remaining)
690 877
         
691 878
         if "date" not in hkeys:
692 879
             self.outheaders.append(("Date", rfc822.formatdate()))
693 880
         
694 881
         if "server" not in hkeys:
695  
-            self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE']))
  882
+            self.outheaders.append(("Server", self.server.server_name))
696 883
         
697  
-        buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"]
698  
-        try:
699  
-            buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
700  
-        except TypeError:
701  
-            if not isinstance(k, str):
702  
-                raise TypeError("WSGI response header key %r is not a string.")
703  
-            if not isinstance(v, str):
704  
-                raise TypeError("WSGI response header value %r is not a string.")
705  
-            else:
706  
-                raise
707  
-        buf.append("\r\n")
708  
-        self.wfile.sendall("".join(buf))
  884
+        buf = [self.server.protocol + " " + self.status + CRLF]
  885
+        for k, v in self.outheaders:
  886
+            buf.append(k + ": " + v + CRLF)
  887
+        buf.append(CRLF)
  888
+        self.conn.wfile.sendall("".join(buf))
709 889
 
710 890
 
711 891
 class NoSSLError(Exception):
@@ -718,38 +898,47 @@ class FatalSSLAlert(Exception):
718 898
     pass
719 899
 
720 900
 
721  
-if not _fileobject_uses_str_type:
722  
-    class CP_fileobject(socket._fileobject):
723  
-        """Faux file object attached to a socket object."""
724  
-
725  
-        def sendall(self, data):
726  
-            """Sendall for non-blocking sockets."""
727  
-            while data:
728  
-                try:
729  
-                    bytes_sent = self.send(data)
730  
-                    data = data[bytes_sent:]
731  
-                except socket.error, e:
732  
-                    if e.args[0] not in socket_errors_nonblocking:
733  
-                        raise
734  
-
735  
-        def send(self, data):
736  
-            return self._sock.send(data)
737  
-
738  
-        def flush(self):
739  
-            if self._wbuf:
740  
-                buffer = "".join(self._wbuf)
741  
-                self._wbuf = []
742  
-                self.sendall(buffer)
743  
-
744  
-        def recv(self, size):
745  
-            while True:
746  
-                try:
747  
-                    return self._sock.recv(size)
748  
-                except socket.error, e:
749  
-                    if (e.args[0] not in socket_errors_nonblocking
750  
-                        and e.args[0] not in socket_error_eintr):
751  
-                        raise
  901
+class CP_fileobject(socket._fileobject):
  902
+    """Faux file object attached to a socket object."""
752 903
 
  904
+    def __init__(self, *args, **kwargs):
  905
+        self.bytes_read = 0
  906
+        self.bytes_written = 0
  907
+        socket._fileobject.__init__(self, *args, **kwargs)
  908
+    
  909
+    def sendall(self, data):
  910
+        """Sendall for non-blocking sockets."""
  911
+        while data:
  912
+            try:
  913
+                bytes_sent = self.send(data)
  914
+                data = data[bytes_sent:]
  915
+            except socket.error, e:
  916
+                if e.args[0] not in socket_errors_nonblocking:
  917
+                    raise
  918
+
  919
+    def send(self, data):
  920
+        bytes_sent = self._sock.send(data)
  921
+        self.bytes_written += bytes_sent
  922
+        return bytes_sent
  923
+
  924
+    def flush(self):
  925
+        if self._wbuf:
  926
+            buffer = "".join(self._wbuf)
  927
+            self._wbuf = []
  928
+            self.sendall(buffer)
  929
+
  930
+    def recv(self, size):
  931
+        while True:
  932
+            try:
  933
+                data = self._sock.recv(size)
  934
+                self.bytes_read += len(data)
  935
+                return data
  936
+            except socket.error, e:
  937
+                if (e.args[0] not in socket_errors_nonblocking
  938
+                    and e.args[0] not in socket_error_eintr):
  939
+                    raise
  940
+
  941
+    if not _fileobject_uses_str_type:
753 942
         def read(self, size=-1):
754 943
             # Use max, disallow tiny reads in a loop as they are very inefficient.
755 944
             # We never leave read() with any leftover data from a new recv() call
@@ -895,39 +1084,7 @@ def readline(self, size=-1):
895 1084
                     buf_len += n
896 1085
                     #assert buf_len == buf.tell()
897 1086
                 return buf.getvalue()
898  
-
899  
-else: