Robert Brewer avatar Robert Brewer committed 2794ee5

gateways: Split WSGI support out of the main server/conn/request classes into separate pluggable Gateway classes. This allows us to more easily support multiple WSGI versions, even experimental ones, as well as non-WSGI protocols. A native protocol that skips WSGI is included (runs about 20% more req/sec than WSGI).

Also reinstated support for Trailer headers, and improved chunked request support.

Comments (0)

Files changed (7)

cherrypy/_cpnative_server.py

+"""Native adapter for serving CherryPy via its builtin server."""
+
+import logging
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+import cherrypy
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil
+from cherrypy import wsgiserver
+
+
+class NativeGateway(wsgiserver.Gateway):
+    
+    recursive = False
+    
+    def respond(self):
+        req = self.req
+        try:
+            # Obtain a Request object from CherryPy
+            local = req.server.bind_addr
+            local = httputil.Host(local[0], local[1], "")
+            remote = req.conn.remote_addr, req.conn.remote_port
+            remote = httputil.Host(remote[0], remote[1], "")
+            
+            scheme = req.scheme
+            sn = cherrypy.tree.script_name(req.uri or "/")
+            if sn is None:
+                self.send_response('404 Not Found', [], [''])
+            else:
+                app = cherrypy.tree.apps[sn]
+                method = req.method
+                path = req.path
+                qs = req.qs or ""
+                headers = req.inheaders.items()
+                rfile = req.rfile
+                prev = None
+                
+                try:
+                    redirections = []
+                    while True:
+                        request, response = app.get_serving(
+                            local, remote, scheme, "HTTP/1.1")
+                        request.multithread = True
+                        request.multiprocess = False
+                        request.app = app
+                        request.prev = prev
+                        
+                        # Run the CherryPy Request object and obtain the response
+                        try:
+                            request.run(method, path, qs, req.request_protocol, headers, rfile)
+                            break
+                        except cherrypy.InternalRedirect, ir:
+                            app.release_serving()
+                            prev = request
+                            
+                            if not self.recursive:
+                                if ir.path in redirections:
+                                    raise RuntimeError("InternalRedirector visited the "
+                                                       "same URL twice: %r" % ir.path)
+                                else:
+                                    # Add the *previous* path_info + qs to redirections.
+                                    if qs:
+                                        qs = "?" + qs
+                                    redirections.append(sn + path + qs)
+                            
+                            # Munge environment and try again.
+                            method = "GET"
+                            path = ir.path
+                            qs = ir.query_string
+                            rfile = StringIO()
+                    
+                    self.send_response(
+                        response.output_status, response.header_list,
+                        response.body)
+                finally:
+                    app.release_serving()
+        except:
+            tb = format_exc()
+            #print tb
+            cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
+            s, h, b = bare_error()
+            self.send_response(s, h, b)
+    
+    def send_response(self, status, headers, body):
+        req = self.req
+        
+        # Set response status
+        req.status = str(status or "500 Server Error")
+        
+        # Set response headers
+        for header, value in headers:
+            req.outheaders.append((header, value))
+        if (req.ready and not req.sent_headers):
+            req.sent_headers = True
+            req.send_headers()
+        
+        # Set response body
+        for seg in body:
+            req.write(seg)
+

cherrypy/_cpreqbody.py

 
 def process_urlencoded(entity):
     """Read application/x-www-form-urlencoded data into entity.params."""
-    if not entity.headers.get(u"Content-Length", u""):
-        # No Content-Length header supplied (or it's 0).
-        # If we went ahead and called rfile.read(), it would hang,
-        # since it cannot determine when to stop reading from the socket.
-        # See http://www.cherrypy.org/ticket/493.
-        # See also http://www.cherrypy.org/ticket/650.
-        # Note also that we expect any HTTP server to have decoded
-        # any message-body that had a transfer-coding, and we expect
-        # the HTTP server to have supplied a Content-Length header
-        # which is valid for the decoded entity-body.
-        raise cherrypy.HTTPError(411)
-    
     qs = entity.fp.read()
     for charset in entity.attempt_charsets:
         try:
         # Length
         self.length = None
         clen = headers.get(u'Content-Length', None)
-        if clen is not None:
+        # If Transfer-Encoding is 'chunked', ignore any Content-Length.
+        if clen is not None and 'chunked' not in headers.get(u'Transfer-Encoding', ''):
             try:
                 self.length = int(clen)
             except ValueError:
                 if strippedline == self.boundary:
                     break
                 if strippedline == endmarker:
-                    self.fp.done = True
+                    self.fp.finish()
                     break
             
             line = delim + line
 inf = Infinity()
 
 
+comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
+    'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection',
+    'Content-Encoding', 'Content-Language', 'Expect', 'If-Match',
+    'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer',
+    'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate']
+
 class SizedReader:
     
-    def __init__(self, fp, length, maxbytes, bufsize=8192):
+    def __init__(self, fp, length, maxbytes, bufsize=8192, has_trailers=False):
         # Wrap our fp in a buffer so peek() works
         self.fp = fp
         self.length = length
         self.bufsize = bufsize
         self.bytes_read = 0
         self.done = False
+        self.has_trailers = has_trailers
     
     def read(self, size=None, fp_out=None):
         """Read bytes from the request body and return or write them to a file.
             if size and size < remaining:
                 remaining = size
         if remaining == 0:
-            self.done = True
+            self.finish()
             if fp_out is None:
                 return ''
             else:
         # Read bytes from the socket.
         while remaining > 0:
             chunksize = min(remaining, self.bufsize)
-            data = self.fp.read(chunksize)
+            try:
+                data = self.fp.read(chunksize)
+            except IOError:
+                raise cherrypy.HTTPError(413)
             if not data:
-                self.done = True
+                self.finish()
                 break
             datalen = len(data)
             remaining -= datalen
             if seen >= sizehint:
                 break
         return lines
+    
+    def finish(self):
+        self.done = True
+        if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
+            self.trailers = {}
+            
+            try:
+                for line in self.fp.read_trailer_lines():
+                    if line[0] in ' \t':
+                        # It's a continuation line.
+                        v = line.strip()
+                    else:
+                        try:
+                            k, v = line.split(":", 1)
+                        except ValueError:
+                            raise ValueError("Illegal header line.")
+                        k = k.strip().title()
+                        v = v.strip()
+                    
+                    if k in comma_separated_headers:
+                        existing = self.trailers.get(envname)
+                        if existing:
+                            v = ", ".join((existing, v))
+                    self.trailers[k] = v
+            except IOError:
+                raise cherrypy.HTTPError(413)
 
 
 class RequestBody(Entity):
             raise cherrypy.HTTPError(411)
         
         self.fp = SizedReader(self.fp, self.length,
-                              self.maxbytes, bufsize=self.bufsize)
+                              self.maxbytes, bufsize=self.bufsize,
+                              has_trailers='Trailer' in h)
         super(RequestBody, self).process()
         
         # Body params should also be a part of the request_params
                 request_params[key].append(value)
             else:
                 request_params[key] = value
+

cherrypy/_cpwsgi.py

 from cherrypy.lib import httputil
 
 
-def downgrade_wsgi_11_to_10(environ):
-    """Return a new environ dict for WSGI 1.0 from the given WSGI 1.1 environ."""
+def downgrade_wsgi_u0_to_10(environ):
+    """Return a new environ dict for WSGI 1.0 from the given WSGI u.0 environ."""
     env10 = {}
     
     enc = environ[u'wsgi.url_encoding']
     def __init__(self, environ, start_response, cpapp, recursive=False):
         self.redirections = []
         self.recursive = recursive
-        if environ.get(u'wsgi.version') == (1, 1):
-            environ = downgrade_wsgi_11_to_10(environ)
+        if environ.get(u'wsgi.version') == ('u', 0):
+            environ = downgrade_wsgi_u0_to_10(environ)
         self.environ = environ
         self.start_response = start_response
         self.cpapp = cpapp

cherrypy/_cpwsgi_server.py

 
 
 class CPHTTPRequest(wsgiserver.HTTPRequest):
-    
-    def __init__(self, rfile, wfile, environ, wsgi_app):
-        s = cherrypy.server
-        self.max_request_header_size = s.max_request_header_size or 0
-        self.max_request_body_size = s.max_request_body_size or 0
-        wsgiserver.HTTPRequest.__init__(self, rfile, wfile, environ, wsgi_app)
+    pass
 
 
 class CPHTTPConnection(wsgiserver.HTTPConnection):
-    
-    RequestHandlerClass = CPHTTPRequest
+    pass
 
 
 class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
     and apply some attributes from config -> cherrypy.server -> wsgiserver.
     """
     
-    ConnectionClass = CPHTTPConnection
-    
     def __init__(self, server_adapter=cherrypy.server):
         self.server_adapter = server_adapter
-        
-        # We have to make custom subclasses of wsgiserver internals here
-        # so that our server.* attributes get applied to every request.
-        class _CPHTTPRequest(wsgiserver.HTTPRequest):
-            def __init__(self, rfile, wfile, environ, wsgi_app):
-                s = server_adapter
-                self.max_request_header_size = s.max_request_header_size or 0
-                self.max_request_body_size = s.max_request_body_size or 0
-                wsgiserver.HTTPRequest.__init__(self, rfile, wfile, environ, wsgi_app)
-        class _CPHTTPConnection(wsgiserver.HTTPConnection):
-            RequestHandlerClass = _CPHTTPRequest
-        self.ConnectionClass = _CPHTTPConnection
+        self.max_request_header_size = self.server_adapter.max_request_header_size or 0
+        self.max_request_body_size = self.server_adapter.max_request_body_size or 0
         
         server_name = (self.server_adapter.socket_host or
                        self.server_adapter.socket_file or
                        None)
         
+        self.wsgi_version = self.server_adapter.wsgi_version
         s = wsgiserver.CherryPyWSGIServer
         s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
                    self.server_adapter.thread_pool,
         self.protocol = self.server_adapter.protocol_version
         self.nodelay = self.server_adapter.nodelay
         
-        self.environ["wsgi.version"] = self.server_adapter.wsgi_version
-        
         if self.server_adapter.ssl_context:
             adapter_class = self.get_ssl_adapter_class()
             s.ssl_adapter = adapter_class(self.server_adapter.ssl_certificate,

cherrypy/test/test_conn.py

             if not cherrypy.request.method == 'POST':
                 raise AssertionError("'POST' != request.method %r" %
                                      cherrypy.request.method)
-            return ("thanks for '%s' (%s)" %
-                    (cherrypy.request.body.read(),
-                     cherrypy.request.headers['Content-Type']))
+            return "thanks for '%s'" % cherrypy.request.body.read()
         upload.exposed = True
         
         def custom(self, response_code):
         response.begin()
         self.status, self.headers, self.body = webtest.shb(response)
         self.assertStatus(200)
-        self.assertBody("thanks for 'I am a small file' (text/plain)")
+        self.assertBody("thanks for 'I am a small file'")
         conn.close()
 
 
             response.begin()
             self.status, self.headers, self.body = webtest.shb(response)
             self.assertStatus(200)
-            self.assertBody("thanks for 'I am a small file' (text/plain)")
+            self.assertBody("thanks for 'I am a small file'")
             conn.close()
     
     def test_No_Message_Body(self):
         
         # Try a normal chunked request (with extensions)
         body = ("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n"
-                "Content-Type: application/x-json\r\n\r\n")
+                "Content-Type: application/x-json\r\n"
+                "\r\n")
         conn.putrequest("POST", "/upload", skip_host=True)
         conn.putheader("Host", self.HOST)
         conn.putheader("Transfer-Encoding", "chunked")
         # Note that this is somewhat malformed:
         # we shouldn't be sending Content-Length.
         # RFC 2616 says the server should ignore it.
-        conn.putheader("Content-Length", "%s" % len(body))
+        conn.putheader("Content-Length", "3")
         conn.endheaders()
         conn.send(body)
         response = conn.getresponse()
         self.status, self.headers, self.body = webtest.shb(response)
         self.assertStatus('200 OK')
-        self.assertBody("thanks for 'xx\r\nxxxxyyyyy' (application/x-json)")
+        self.assertBody("thanks for 'xx\r\nxxxxyyyyy'")
         
         # Try a chunked request that exceeds server.max_request_body_size.
         # Note that the delimiters and trailer are included.
         response = conn.getresponse()
         self.status, self.headers, self.body = webtest.shb(response)
         self.assertStatus(413)
-        self.assertBody("")
         conn.close()
     
     def test_Content_Length(self):

cherrypy/test/test_objectmapping.py

             return "bar"
         foobar.exposed = True
         
-        def default(self, *params):
+        def default(self, *params, **kwargs):
             return "default:" + repr(params)
         default.exposed = True
         

cherrypy/wsgiserver/__init__.py

-"""A high-speed, production ready, thread pooled, generic WSGI server.
+"""A high-speed, production ready, thread pooled, generic HTTP server.
 
 Simplest example on how to use this module directly
 (without using CherryPy's application machinery):
 Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance.
 
 This won't call the CherryPy engine (application side) at all, only the
-WSGI server, which is independent from the rest of CherryPy. Don't
+HTTP server, which is independent from the rest of CherryPy. Don't
 let the name "CherryPyWSGIServer" throw you; the name merely reflects
 its origin, not its coupling.
 
                 req.parse_request()
                 ->  # Read the Request-Line, e.g. "GET /page HTTP/1.1"
                     req.rfile.readline()
-                    req.read_headers()
+                    read_headers(req.rfile, req.inheaders)
                 req.respond()
-                ->  response = wsgi_app(...)
+                ->  response = app(...)
                     try:
                         for chunk in response:
                             if chunk:
 socket_errors_nonblocking = plat_specific_errors(
     'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
 
-comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
-    'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
-    'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
-    'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
-    'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
-    'WWW-AUTHENTICATE']
+comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
+    'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control',
+    'Connection', 'Content-Encoding', 'Content-Language', 'Expect',
+    'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE',
+    'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning',
+    'WWW-Authenticate']
 
 
-class WSGIPathInfoDispatcher(object):
-    """A WSGI dispatcher for dispatch based on the PATH_INFO.
+def read_headers(rfile, hdict=None):
+    """Read headers from the given stream into the given header dict.
     
-    apps: a dict or list of (path_prefix, app) pairs.
+    If hdict is None, a new header dict is created. Returns the populated
+    header dict.
+    
+    Headers which are repeated are folded together using a comma if their
+    specification so dictates.
+    
+    This function raises ValueError when the read bytes violate the HTTP spec.
+    You should probably return "400 Bad Request" if this happens.
     """
+    if hdict is None:
+        hdict = {}
     
-    def __init__(self, apps):
-        try:
-            apps = apps.items()
-        except AttributeError:
-            pass
+    while True:
+        line = rfile.readline()
+        if not line:
+            # No more data--illegal end of headers
+            raise ValueError("Illegal end of headers.")
         
-        # Sort the apps by len(path), descending
-        apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0])))
-        apps.reverse()
+        if line == CRLF:
+            # Normal end of headers
+            break
+        if not line.endswith(CRLF):
+            raise ValueError("HTTP requires CRLF terminators")
         
-        # The path_prefix strings must start, but not end, with a slash.
-        # Use "" instead of "/".
-        self.apps = [(p.rstrip("/"), a) for p, a in apps]
+        if line[0] in ' \t':
+            # It's a continuation line.
+            v = line.strip()
+        else:
+            try:
+                k, v = line.split(":", 1)
+            except ValueError:
+                raise ValueError("Illegal header line.")
+            # TODO: what about TE and WWW-Authenticate?
+            k = k.strip().title()
+            v = v.strip()
+            hname = k
+        
+        if k in comma_separated_headers:
+            existing = hdict.get(hname)
+            if existing:
+                v = ", ".join((existing, v))
+        hdict[hname] = v
     
-    def __call__(self, environ, start_response):
-        path = environ["PATH_INFO"] or "/"
-        for p, app in self.apps:
-            # The apps list should be sorted by length, descending.
-            if path.startswith(p + "/") or path == p:
-                environ = environ.copy()
-                environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
-                environ["PATH_INFO"] = path[len(p):]
-                return app(environ, start_response)
-        
-        start_response('404 Not Found', [('Content-Type', 'text/plain'),
-                                         ('Content-Length', '0')])
-        return ['']
+    return hdict
 
 
 class MaxSizeExceeded(Exception):
         return data
 
 
+class ChunkedRFile(object):
+    """Wraps a file-like object, returning an empty string when exhausted.
+    
+    This class is intended to provide a conforming wsgi.input value for
+    request entities that have been encoded with the 'chunked' transfer
+    encoding.
+    """
+    
+    def __init__(self, rfile, maxlen, bufsize=8192):
+        self.rfile = rfile
+        self.maxlen = maxlen
+        self.bytes_read = 0
+        self.buffer = ''
+        self.bufsize = bufsize
+        self.closed = False
+    
+    def _fetch(self):
+        if self.closed:
+            return
+        
+        line = self.rfile.readline()
+        self.bytes_read += len(line)
+        
+        if self.maxlen and self.bytes_read > self.maxlen:
+            raise IOError("Request Entity Too Large")
+        
+        line = line.strip().split(";", 1)
+        
+        try:
+            chunk_size = line.pop(0)
+            chunk_size = int(chunk_size, 16)
+        except ValueError:
+            raise ValueError("Bad chunked transfer size: " + repr(chunk_size))
+        
+        if chunk_size <= 0:
+            self.closed = True
+            return
+        
+##            if line: chunk_extension = line[0]
+        
+        if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
+            raise IOError("Request Entity Too Large")
+        
+        chunk = self.rfile.read(chunk_size)
+        self.bytes_read += len(chunk)
+        self.buffer += chunk
+        
+        crlf = self.rfile.read(2)
+        if crlf != CRLF:
+            raise ValueError(
+                 "Bad chunked transfer coding (expected '\\r\\n', "
+                 "got " + repr(crlf) + ")")
+    
+    def read(self, size=None):
+        data = ''
+        while True:
+            if size and len(data) >= size:
+                return data
+            
+            if not self.buffer:
+                self._fetch()
+                if not self.buffer:
+                    # EOF
+                    return data
+            
+            if size:
+                remaining = size - len(data)
+                data += self.buffer[:remaining]
+                self.buffer = self.buffer[remaining:]
+            else:
+                data += self.buffer
+    
+    def readline(self, size=None):
+        data = ''
+        while True:
+            if size and len(data) >= size:
+                return data
+            
+            if not self.buffer:
+                self._fetch()
+                if not self.buffer:
+                    # EOF
+                    return data
+            
+            newline_pos = self.buffer.find('\n')
+            if size:
+                if newline_pos == -1:
+                    remaining = size - len(data)
+                    data += self.buffer[:remaining]
+                    self.buffer = self.buffer[remaining:]
+                else:
+                    remaining = min(size - len(data), newline_pos)
+                    data += self.buffer[:remaining]
+                    self.buffer = self.buffer[remaining:]
+            else:
+                if newline_pos == -1:
+                    data += self.buffer
+                else:
+                    data += self.buffer[:newline_pos]
+                    self.buffer = self.buffer[newline_pos:]
+    
+    def readlines(self, sizehint=0):
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline(sizehint)
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline(sizehint)
+        return lines
+    
+    def read_trailer_lines(self):
+        if not self.closed:
+            raise ValueError(
+                "Cannot read trailers until the request body has been read.")
+        
+        while True:
+            line = self.rfile.readline()
+            if not line:
+                # No more data--illegal end of headers
+                raise ValueError("Illegal end of headers.")
+            
+            self.bytes_read += len(line)
+            if self.maxlen and self.bytes_read > self.maxlen:
+                raise IOError("Request Entity Too Large")
+            
+            if line == CRLF:
+                # Normal end of headers
+                break
+            if not line.endswith(CRLF):
+                raise ValueError("HTTP requires CRLF terminators")
+            
+            yield line
+    
+    def close(self):
+        self.rfile.close()
+    
+    def __iter__(self):
+        # Shamelessly stolen from StringIO
+        total = 0
+        line = self.readline(sizehint)
+        while line:
+            yield line
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline(sizehint)
+
+
 class HTTPRequest(object):
     """An HTTP Request (and response).
     
     A single HTTP connection may consist of multiple request/response pairs.
     
-    send: the 'send' method from the connection's socket object.
-    wsgi_app: the WSGI application to call.
-    environ: a partial WSGI environ (server and connection entries).
-        Because this server supports both WSGI 1.0 and 1.1, this attribute is
-        neither; instead, it has unicode keys and byte string values. It is
-        converted to the appropriate WSGI version when the WSGI app is called.
-        
-        The caller MUST set the following entries (because this class doesn't):
-        * All wsgi.* entries except .input and .url_encoding
-        * SERVER_NAME and SERVER_PORT
-        * Any SSL_* entries
-        * Any custom entries like REMOTE_ADDR and REMOTE_PORT
-        * SERVER_SOFTWARE: the value to write in the "Server" response header.
-        * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of
-            the response. From RFC 2145: "An HTTP server SHOULD send a
-            response version equal to the highest version for which the
-            server is at least conditionally compliant, and whose major
-            version is less than or equal to the one received in the
-            request.  An HTTP server MUST NOT send a version for which
-            it is not at least conditionally compliant."
+    server: the Server object which is receiving this request.
+    conn: the HTTPConnection object on which this request connected.
     
+    inheaders: a dict of request headers.
     outheaders: a list of header tuples to write in the response.
     ready: when True, the request has been parsed and is ready to begin
         generating the response. When False, signals the calling Connection
         send_headers.
     """
     
-    max_request_header_size = 0
-    max_request_body_size = 0
+    scheme = "http"
     
-    def __init__(self, rfile, wfile, environ, wsgi_app):
-        self._rfile = rfile
-        self.rfile = rfile
-        self.wfile = wfile
-        self.environ = environ.copy()
-        self.wsgi_app = wsgi_app
+    def __init__(self, server, conn):
+        self.server= server
+        self.conn = conn
         
         self.ready = False
         self.started_request = False
-        self.started_response = False
         self.status = ""
+        self.inheaders = {}
         self.outheaders = []
         self.sent_headers = False
         self.close_connection = False
     
     def parse_request(self):
         """Parse the next HTTP request start-line and message-headers."""
-        self.rfile = SizeCheckWrapper(self._rfile, self.max_request_header_size)
+        self.rfile = SizeCheckWrapper(self.conn.rfile,
+                                      self.server.max_request_header_size)
         try:
             self._parse_request()
         except MaxSizeExceeded:
             self.simple_response(400, "HTTP requires CRLF terminators")
             return
         
-        environ = self.environ
-        
         try:
             method, uri, req_protocol = request_line.strip().split(" ", 2)
         except ValueError:
             self.simple_response(400, "Malformed Request-Line")
             return
         
-        environ["REQUEST_URI"] = uri
-        environ["REQUEST_METHOD"] = method
+        self.uri = uri
+        self.method = method
         
         # uri may be an abs_path (including "http://host.domain.tld");
         scheme, authority, path = self.parse_request_uri(uri)
             return
         
         if scheme:
-            environ["wsgi.url_scheme"] = scheme
-        
-        environ["SCRIPT_NAME"] = ""
+            self.scheme = scheme
         
         qs = ''
         if '?' in path:
             path, qs = path.split('?', 1)
         
-        # Unquote the path+params (e.g. "/this%20path" -> "this path").
+        # Unquote the path+params (e.g. "/this%20path" -> "/this path").
         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
         #
         # But note that "...a URI must be separated into its components
         # before the escaped characters within those components can be
         # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
+        # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path".
         try:
             atoms = [unquote(x) for x in quoted_slash.split(path)]
         except ValueError, ex:
             self.simple_response("400 Bad Request", ex.args[0])
             return
         path = "%2F".join(atoms)
-        environ["PATH_INFO"] = path
+        self.path = path
         
-        # Note that, like wsgiref and most other WSGI servers,
+        # Note that, like wsgiref and most other HTTP servers,
         # we "% HEX HEX"-unquote the path but not the query string.
-        environ["QUERY_STRING"] = qs
+        self.qs = qs
         
         # Compare request and server HTTP protocol versions, in case our
         # server does not support the requested protocol. Limit our output
         # the client only understands 1.0. RFC 2616 10.5.6 says we should
         # only return 505 if the _major_ version is different.
         rp = int(req_protocol[5]), int(req_protocol[7])
-        server_protocol = environ["ACTUAL_SERVER_PROTOCOL"]
-        sp = int(server_protocol[5]), int(server_protocol[7])
+        sp = int(self.server.protocol[5]), int(self.server.protocol[7])
         
         if sp[0] != rp[0]:
             self.simple_response("505 HTTP Version Not Supported")
             return
-        # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
-        environ["SERVER_PROTOCOL"] = req_protocol
+        self.request_protocol = req_protocol
         self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
         
         # then all the http headers
         try:
-            self.read_headers()
+            read_headers(self.rfile, self.inheaders)
         except ValueError, ex:
             self.simple_response("400 Bad Request", ex.args[0])
             return
         
-        mrbs = self.max_request_body_size
-        if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs:
+        mrbs = self.server.max_request_body_size
+        if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs:
             self.simple_response("413 Request Entity Too Large")
             return
         
         # Persistent connection support
         if self.response_protocol == "HTTP/1.1":
             # Both server and client are HTTP/1.1
-            if environ.get("HTTP_CONNECTION", "") == "close":
+            if self.inheaders.get("Connection", "") == "close":
                 self.close_connection = True
         else:
             # Either the server or client (or both) are HTTP/1.0
-            if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
+            if self.inheaders.get("Connection", "") != "Keep-Alive":
                 self.close_connection = True
         
         # Transfer-Encoding support
         te = None
         if self.response_protocol == "HTTP/1.1":
-            te = environ.get("HTTP_TRANSFER_ENCODING")
+            te = self.inheaders.get("Transfer-Encoding")
             if te:
                 te = [x.strip().lower() for x in te.split(",") if x.strip()]
         
         #
         # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
         # but it seems like it would be a big slowdown for such a rare case.
-        if environ.get("HTTP_EXPECT", "") == "100-continue":
+        if self.inheaders.get("Expect", "") == "100-continue":
             # Don't use simple_response here, because it emits headers
             # we don't want. See http://www.cherrypy.org/ticket/951
-            msg = self.environ['ACTUAL_SERVER_PROTOCOL'] + " 100 Continue\r\n\r\n"
+            msg = self.server.protocol + " 100 Continue\r\n\r\n"
             try:
-                self.wfile.sendall(msg)
+                self.conn.wfile.sendall(msg)
             except socket.error, x:
                 if x.args[0] not in socket_errors_to_ignore:
                     raise
             # An authority.
             return None, uri, None
     
-    
-    def read_headers(self):
-        """Read header lines from the incoming stream."""
-        environ = self.environ
-        
-        while True:
-            line = self.rfile.readline()
-            if not line:
-                # No more data--illegal end of headers
-                raise ValueError("Illegal end of headers.")
-            
-            if line == CRLF:
-                # Normal end of headers
-                break
-            if not line.endswith(CRLF):
-                raise ValueError("HTTP requires CRLF terminators")
-            
-            if line[0] in ' \t':
-                # It's a continuation line.
-                v = line.strip()
-            else:
-                try:
-                    k, v = line.split(":", 1)
-                except ValueError:
-                    raise ValueError("Illegal header line.")
-                k = k.strip().decode('ISO-8859-1').upper()
-                v = v.strip()
-                envname = "HTTP_" + k.replace("-", "_")
-            
-            if k in comma_separated_headers:
-                existing = environ.get(envname)
-                if existing:
-                    v = ", ".join((existing, v))
-            environ[envname] = v
-        
-        ct = environ.pop("HTTP_CONTENT_TYPE", None)
-        if ct is not None:
-            environ["CONTENT_TYPE"] = ct
-        cl = environ.pop("HTTP_CONTENT_LENGTH", None)
-        if cl is not None:
-            environ["CONTENT_LENGTH"] = cl
-    
-    def decode_chunked(self):
-        """Decode the 'chunked' transfer coding."""
-        self.rfile = SizeCheckWrapper(self._rfile, self.max_request_body_size)
-        cl = 0
-        data = StringIO.StringIO()
-        while True:
-            line = self.rfile.readline().strip().split(";", 1)
-            try:
-                chunk_size = line.pop(0)
-                chunk_size = int(chunk_size, 16)
-            except ValueError:
-                self.simple_response("400 Bad Request",
-                     "Bad chunked transfer size: " + repr(chunk_size))
-                return
-            if chunk_size <= 0:
-                break
-##            if line: chunk_extension = line[0]
-            cl += chunk_size
-            data.write(self.rfile.read(chunk_size))
-            crlf = self.rfile.read(2)
-            if crlf != CRLF:
-                self.simple_response("400 Bad Request",
-                     "Bad chunked transfer coding (expected '\\r\\n', "
-                     "got " + repr(crlf) + ")")
-                return
-        
-        # Grab any trailer headers
-        self.read_headers()
-        
-        data.seek(0)
-        self.rfile = data
-        self.environ["CONTENT_LENGTH"] = str(cl) or ""
-        return True
-    
     def respond(self):
-        """Call the appropriate WSGI app and write its iterable output."""
+        """Call the gateway and write its iterable output."""
+        mrbs = self.server.max_request_body_size
         if self.chunked_read:
-            # If chunked, Content-Length will be 0.
-            try:
-                if not self.decode_chunked():
-                    self.close_connection = True
-                    return
-            except MaxSizeExceeded:
-                self.simple_response("413 Request Entity Too Large")
-                return
+            self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
         else:
-            cl = int(self.environ.get("CONTENT_LENGTH", 0))
-            if self.max_request_body_size and self.max_request_body_size < cl:
+            cl = int(self.inheaders.get("Content-Length", 0))
+            if mrbs and mrbs < cl:
                 if not self.sent_headers:
                     self.simple_response("413 Request Entity Too Large")
                 return
-            self.rfile = KnownLengthRFile(self._rfile, cl)
+            self.rfile = KnownLengthRFile(self.conn.rfile, cl)
         
-        self.environ["wsgi.input"] = self.rfile
-        self._respond()
-    
-    def _respond(self):
-        env = self.get_version_specific_environ()
-        #for k, v in sorted(env.items()):
-        #    print(k, '=', v)
-        response = self.wsgi_app(env, self.start_response)
-        try:
-            for chunk in response:
-                # "The start_response callable must not actually transmit
-                # the response headers. Instead, it must store them for the
-                # server or gateway to transmit only after the first
-                # iteration of the application return value that yields
-                # a NON-EMPTY string, or upon the application's first
-                # invocation of the write() callable." (PEP 333)
-                if chunk:
-                    if isinstance(chunk, unicode):
-                        chunk = chunk.encode('ISO-8859-1')
-                    self.write(chunk)
-        finally:
-            if hasattr(response, "close"):
-                response.close()
+        self.server.gateway(self).respond()
         
         if (self.ready and not self.sent_headers):
             self.sent_headers = True
             self.send_headers()
         if self.chunked_write:
-            self.wfile.sendall("0\r\n\r\n")
-    
-    def get_version_specific_environ(self):
-        """Return a new environ dict targeting the given wsgi.version"""
-        # Note that our internal environ type has keys decoded with ISO-8859-1
-        # but byte string values.
-        if self.environ["wsgi.version"] == (1, 0):
-            # Encode all keys.
-            env10 = {}
-            for k, v in self.environ.items():
-                if isinstance(k, unicode):
-                    k = k.encode('ISO-8859-1')
-                env10[k] = v
-            return env10
-        
-        env11 = self.environ.copy()
-        
-        # Request-URI
-        env11.setdefault('wsgi.url_encoding', 'utf-8')
-        try:
-            for key in ["PATH_INFO", "SCRIPT_NAME", "QUERY_STRING"]:
-                env11[key] = self.environ[key].decode(env11['wsgi.url_encoding'])
-        except UnicodeDecodeError:
-            # Fall back to latin 1 so apps can transcode if needed.
-            env11['wsgi.url_encoding'] = 'ISO-8859-1'
-            for key in ["PATH_INFO", "SCRIPT_NAME", "QUERY_STRING"]:
-                env11[key] = self.environ[key].decode(env11['wsgi.url_encoding'])
-        
-        for k, v in sorted(env11.items()):
-            if isinstance(v, str) and k not in (
-                'REQUEST_URI', 'PATH_INFO', 'SCRIPT_NAME', 'QUERY_STRING',
-                'wsgi.input'):
-                env11[k] = v.decode('ISO-8859-1')
-        
-        return env11
+            self.conn.wfile.sendall("0\r\n\r\n")
     
     def simple_response(self, status, msg=""):
         """Write a simple response back to the client."""
         status = str(status)
-        buf = [self.environ['ACTUAL_SERVER_PROTOCOL'] + " " +
+        buf = [self.server.protocol + " " +
                status + CRLF,
                "Content-Length: %s\r\n" % len(msg),
                "Content-Type: text/plain\r\n"]
             buf.append(msg)
         
         try:
-            self.wfile.sendall("".join(buf))
+            self.conn.wfile.sendall("".join(buf))
         except socket.error, x:
             if x.args[0] not in socket_errors_to_ignore:
                 raise
     
-    def start_response(self, status, headers, exc_info = None):
-        """WSGI callable to begin the HTTP response."""
-        # "The application may call start_response more than once,
-        # if and only if the exc_info argument is provided."
-        if self.started_response and not exc_info:
-            raise AssertionError("WSGI start_response called a second "
-                                 "time with no exc_info.")
-        
-        # "if exc_info is provided, and the HTTP headers have already been
-        # sent, start_response must raise an error, and should raise the
-        # exc_info tuple."
-        if self.sent_headers:
-            try:
-                raise exc_info[0], exc_info[1], exc_info[2]
-            finally:
-                exc_info = None
-        
-        self.started_response = True
-        self.status = status
-        self.outheaders.extend(headers)
-        return self.write
-    
     def write(self, chunk):
-        """WSGI callable to write unbuffered data to the client.
-        
-        This method is also used internally by start_response (to write
-        data from the iterable returned by the WSGI application).
-        """
-        if not self.started_response:
-            raise AssertionError("WSGI write called before start_response.")
-        
-        if not self.sent_headers:
-            self.sent_headers = True
-            self.send_headers()
-        
+        """Write unbuffered data to the client."""
         if self.chunked_write and chunk:
             buf = [hex(len(chunk))[2:], CRLF, chunk, CRLF]
-            self.wfile.sendall("".join(buf))
+            self.conn.wfile.sendall("".join(buf))
         else:
-            self.wfile.sendall(chunk)
+            self.conn.wfile.sendall(chunk)
     
     def send_headers(self):
-        """Assert, process, and send the HTTP response message-headers."""
+        """Assert, process, and send the HTTP response message-headers.
+        
+        You must set self.status, and self.outheaders before calling this.
+        """
         hkeys = [key.lower() for key, value in self.outheaders]
         status = int(self.status[:3])
         
                 pass
             else:
                 if (self.response_protocol == 'HTTP/1.1'
-                    and self.environ["REQUEST_METHOD"] != 'HEAD'):
+                    and self.method != 'HEAD'):
                     # Use the chunked transfer-coding
                     self.chunked_write = True
                     self.outheaders.append(("Transfer-Encoding", "chunked"))
             self.outheaders.append(("Date", rfc822.formatdate()))
         
         if "server" not in hkeys:
-            self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE']))
+            self.outheaders.append(("Server", self.server.server_name))
         
-        buf = [self.environ['ACTUAL_SERVER_PROTOCOL'] +
-               " " + self.status + CRLF]
-        try:
-            for k, v in self.outheaders:
-                buf.append(k + ": " + v + "\r\n")
-        except TypeError:
-            if not isinstance(k, str):
-                raise TypeError("WSGI response header key %r is not a byte string." % k)
-            if not isinstance(v, str):
-                raise TypeError("WSGI response header value %r is not a byte string." % v)
-            else:
-                raise
+        buf = [self.server.protocol + " " + self.status + CRLF]
+        for k, v in self.outheaders:
+            buf.append(k + ": " + v + "\r\n")
         buf.append(CRLF)
-        self.wfile.sendall("".join(buf))
+        self.conn.wfile.sendall("".join(buf))
 
 
 class NoSSLError(Exception):
 class HTTPConnection(object):
     """An HTTP connection (active socket).
     
+    server: the Server object which received this connection.
     socket: the raw socket object (usually TCP) for this connection.
-    wsgi_app: the WSGI application for this server/connection.
-    environ: a WSGI environ template. This will be copied for each request.
-    
-    rfile: a fileobject for reading from the socket.
-    send: a function for writing (+ flush) to the socket.
+    makefile: a fileobject class for reading from the socket.
     """
     
+    remote_addr = None
+    remote_port = None
+    ssl_env = None
     rbufsize = -1
     RequestHandlerClass = HTTPRequest
-    environ = {"wsgi.url_scheme": "http",
-               "wsgi.multithread": True,
-               "wsgi.multiprocess": False,
-               "wsgi.run_once": False,
-               "wsgi.errors": sys.stderr,
-               }
     
-    def __init__(self, sock, wsgi_app, environ, makefile=CP_fileobject):
+    def __init__(self, server, sock, makefile=CP_fileobject):
+        self.server = server
         self.socket = sock
-        self.wsgi_app = wsgi_app
-        
-        # Copy the class environ into self.
-        self.environ = self.environ.copy()
-        self.environ.update(environ)
-        
         self.rfile = makefile(sock, "rb", self.rbufsize)
         self.wfile = makefile(sock, "wb", -1)
     
                 # the RequestHandlerClass constructor, the error doesn't
                 # get written to the previous request.
                 req = None
-                req = self.RequestHandlerClass(
-                    self.rfile, self.wfile, self.environ, self.wsgi_app)
+                req = self.RequestHandlerClass(self.server, self)
                 
                 # This order of operations should guarantee correct pipelining.
                 req.parse_request()
         except NoSSLError:
             if req and not req.sent_headers:
                 # Unwrap our wfile
-                req.wfile = CP_fileobject(self.socket._sock, "wb", -1)
+                req.conn.wfile = CP_fileobject(self.socket._sock, "wb", -1)
                 req.simple_response("400 Bad Request",
                     "The client sent a plain HTTP request, but "
                     "this server only speaks HTTPS on this port.")
         for i in range(self.min):
             self._threads.append(WorkerThread(self.server))
         for worker in self._threads:
-            worker.setName("CP WSGIServer " + worker.getName())
+            worker.setName("CP Server " + worker.getName())
             worker.start()
         for worker in self._threads:
             while not worker.ready:
             if self.max > 0 and len(self._threads) >= self.max:
                 break
             worker = WorkerThread(self.server)
-            worker.setName("CP WSGIServer " + worker.getName())
+            worker.setName("CP Server " + worker.getName())
             self._threads.append(worker)
             worker.start()
     
         raise NotImplemented
 
 
-class CherryPyWSGIServer(object):
-    """An HTTP server for WSGI.
+class CherryPyHTTPServer(object):
+    """An HTTP server.
     
     bind_addr: The interface on which to listen for connections.
         For TCP sockets, a (host, port) tuple. Host values may be any IPv4
         IPv6. The empty string or None are not allowed.
         
         For UNIX sockets, supply the filename as a string.
-    wsgi_app: the WSGI 'application callable'; multiple WSGI applications
-        may be passed as (path_prefix, app) pairs.
-    numthreads: the number of worker threads to create (default 10).
-    server_name: the string to set for WSGI's SERVER_NAME environ entry.
-        Defaults to socket.gethostname().
-    max: the maximum number of queued requests (defaults to -1 = no limit).
+    gateway: a Gateway instance.
+    minthreads: the minimum number of worker threads to create (default 10).
+    maxthreads: the maximum number of worker threads to create (default -1 = no limit).
+    server_name: defaults to socket.gethostname().
+    
     request_queue_size: the 'backlog' argument to socket.listen();
         specifies the maximum number of queued connections (default 5).
     timeout: the timeout in seconds for accepted connections (default 10).
-    
     nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket
         option.
-    
     protocol: the version string to write in the Status-Line of all
         HTTP responses. For example, "HTTP/1.1" (the default). This
         also limits the supported features used in the response.
     protocol = "HTTP/1.1"
     _bind_addr = "127.0.0.1"
     version = "CherryPy/3.2.0beta"
+    response_header = None
     ready = False
     _interrupt = None
-    
+    max_request_header_size = 0
+    max_request_body_size = 0
     nodelay = True
     
     ConnectionClass = HTTPConnection
-    environ = {"wsgi.version": (1, 0)}
     
     ssl_adapter = None
     
-    def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
-                 max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
-        self.requests = ThreadPool(self, min=numthreads or 1, max=max)
-        self.environ = self.environ.copy()
+    def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1,
+                 server_name=None):
+        self.bind_addr = bind_addr
+        self.gateway = gateway
         
-        self.wsgi_app = wsgi_app
+        self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads)
         
-        self.bind_addr = bind_addr
         if not server_name:
             server_name = socket.gethostname()
         self.server_name = server_name
-        self.request_queue_size = request_queue_size
-        
-        self.timeout = timeout
-        self.shutdown_timeout = shutdown_timeout
-    
-    def _get_numthreads(self):
-        return self.requests.min
-    def _set_numthreads(self, value):
-        self.requests.min = value
-    numthreads = property(_get_numthreads, _set_numthreads)
     
     def __str__(self):
         return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
             if hasattr(s, 'settimeout'):
                 s.settimeout(self.timeout)
             
-            environ = self.environ.copy()
-            # SERVER_SOFTWARE is common for IIS. It's also helpful for
-            # us to pass a default value for the "Server" response header.
-            if environ.get("SERVER_SOFTWARE") is None:
-                environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
-            # set a non-standard environ entry so the WSGI app can know what
-            # the *real* server protocol is (and what features to support).
-            # See http://www.faqs.org/rfcs/rfc2145.html.
-            environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol
-            environ["SERVER_NAME"] = self.server_name
-            
-            if isinstance(self.bind_addr, basestring):
-                # AF_UNIX. This isn't really allowed by WSGI, which doesn't
-                # address unix domain sockets. But it's better than nothing.
-                environ["SERVER_PORT"] = ""
-            else:
-                environ["SERVER_PORT"] = str(self.bind_addr[1])
-                # optional values
-                # Until we do DNS lookups, omit REMOTE_HOST
-                if addr is None: # sometimes this can happen
-                    # figure out if AF_INET or AF_INET6.
-                    if len(s.getsockname()) == 2:
-                        # AF_INET
-                        addr = ('0.0.0.0', 0)
-                    else:
-                        # AF_INET6
-                        addr = ('::', 0)
-                environ["REMOTE_ADDR"] = addr[0]
-                environ["REMOTE_PORT"] = str(addr[1])
+            if self.response_header is None:
+                self.response_header = "%s Server" % self.version
             
             makefile = CP_fileobject
+            ssl_env = {}
             # if ssl cert and key are set, we try to be a secure HTTP server
             if self.ssl_adapter is not None:
                 try:
                     return
                 if not s:
                     return
-                environ.update(ssl_env)
                 makefile = self.ssl_adapter.makefile
             
-            conn = self.ConnectionClass(s, self.wsgi_app, environ, makefile)
+            conn = self.ConnectionClass(self, s, makefile)
+            
+            if not isinstance(self.bind_addr, basestring):
+                # optional values
+                # Until we do DNS lookups, omit REMOTE_HOST
+                if addr is None: # sometimes this can happen
+                    # figure out if AF_INET or AF_INET6.
+                    if len(s.getsockname()) == 2:
+                        # AF_INET
+                        addr = ('0.0.0.0', 0)
+                    else:
+                        # AF_INET6
+                        addr = ('::', 0)
+                conn.remote_addr = addr[0]
+                conn.remote_port = addr[1]
+            
+            conn.ssl_env = ssl_env
+            
             self.requests.put(conn)
         except socket.timeout:
             # The only reason for the timeout in start() is so we can
         
         self.requests.stop(self.shutdown_timeout)
 
+
+class Gateway(object):
+    
+    def __init__(self, req):
+        self.req = req
+    
+    def respond(self):
+        raise NotImplemented
+
+# -------------------------------- WSGI Stuff -------------------------------- #
+
+
+class CherryPyWSGIServer(CherryPyHTTPServer):
+    
+    wsgi_version = (1, 0)
+    
+    def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
+                 max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
+        self.requests = ThreadPool(self, min=numthreads or 1, max=max)
+        self.wsgi_app = wsgi_app
+        self.gateway = wsgi_gateways[self.wsgi_version]
+        
+        self.bind_addr = bind_addr
+        if not server_name:
+            server_name = socket.gethostname()
+        self.server_name = server_name
+        self.request_queue_size = request_queue_size
+        
+        self.timeout = timeout
+        self.shutdown_timeout = shutdown_timeout
+    
+    def _get_numthreads(self):
+        return self.requests.min
+    def _set_numthreads(self, value):
+        self.requests.min = value
+    numthreads = property(_get_numthreads, _set_numthreads)
+
+
+class WSGIGateway(Gateway):
+    
+    def __init__(self, req):
+        self.req = req
+        self.started_response = False
+        self.env = self.get_environ()
+    
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version"""
+        raise NotImplemented
+    
+    def respond(self):
+        response = self.req.server.wsgi_app(self.env, self.start_response)
+        try:
+            for chunk in response:
+                # "The start_response callable must not actually transmit
+                # the response headers. Instead, it must store them for the
+                # server or gateway to transmit only after the first
+                # iteration of the application return value that yields
+                # a NON-EMPTY string, or upon the application's first
+                # invocation of the write() callable." (PEP 333)
+                if chunk:
+                    if isinstance(chunk, unicode):
+                        chunk = chunk.encode('ISO-8859-1')
+                    self.write(chunk)
+        finally:
+            if hasattr(response, "close"):
+                response.close()
+    
+    def start_response(self, status, headers, exc_info = None):
+        """WSGI callable to begin the HTTP response."""
+        # "The application may call start_response more than once,
+        # if and only if the exc_info argument is provided."
+        if self.started_response and not exc_info:
+            raise AssertionError("WSGI start_response called a second "
+                                 "time with no exc_info.")
+        self.started_response = True
+        
+        # "if exc_info is provided, and the HTTP headers have already been
+        # sent, start_response must raise an error, and should raise the
+        # exc_info tuple."
+        if self.req.sent_headers:
+            try:
+                raise exc_info[0], exc_info[1], exc_info[2]
+            finally:
+                exc_info = None
+        
+        self.req.status = status
+        for k, v in headers:
+            if not isinstance(k, str):
+                raise TypeError("WSGI response header key %r is not a byte string." % k)
+            if not isinstance(v, str):
+                raise TypeError("WSGI response header value %r is not a byte string." % v)
+        self.req.outheaders.extend(headers)
+        
+        return self.write
+    
+    def write(self, chunk):
+        """WSGI callable to write unbuffered data to the client.
+        
+        This method is also used internally by start_response (to write
+        data from the iterable returned by the WSGI application).
+        """
+        if not self.started_response:
+            raise AssertionError("WSGI write called before start_response.")
+        
+        if not self.req.sent_headers:
+            self.req.sent_headers = True
+            self.req.send_headers()
+        
+        self.req.write(chunk)
+
+
+class WSGIGateway_10(WSGIGateway):
+    
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version"""
+        req = self.req
+        env = {
+            # set a non-standard environ entry so the WSGI app can know what
+            # the *real* server protocol is (and what features to support).
+            # See http://www.faqs.org/rfcs/rfc2145.html.
+            'ACTUAL_SERVER_PROTOCOL': req.server.protocol,
+            'PATH_INFO': req.path,
+            'QUERY_STRING': req.qs,
+            'REMOTE_ADDR': req.conn.remote_addr or '',
+            'REMOTE_PORT': str(req.conn.remote_port or ''),
+            'REQUEST_METHOD': req.method,
+            'REQUEST_URI': req.uri,
+            'SCRIPT_NAME': '',
+            'SERVER_NAME': req.server.server_name,
+            # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
+            'SERVER_PROTOCOL': req.request_protocol,
+            'wsgi.errors': sys.stderr,
+            'wsgi.input': req.rfile,
+            'wsgi.multiprocess': False,
+            'wsgi.multithread': True,
+            'wsgi.run_once': False,
+            'wsgi.url_scheme': req.scheme,
+            'wsgi.version': (1, 0),
+            }
+        
+        if isinstance(req.server.bind_addr, basestring):
+            # AF_UNIX. This isn't really allowed by WSGI, which doesn't
+            # address unix domain sockets. But it's better than nothing.
+            env["SERVER_PORT"] = ""
+        else:
+            env["SERVER_PORT"] = str(req.server.bind_addr[1])
+        
+        # CONTENT_TYPE/CONTENT_LENGTH
+        for k, v in req.inheaders.iteritems():
+            env["HTTP_" + k.upper().replace("-", "_")] = v
+        ct = env.pop("HTTP_CONTENT_TYPE", None)
+        if ct is not None:
+            env["CONTENT_TYPE"] = ct
+        cl = env.pop("HTTP_CONTENT_LENGTH", None)
+        if cl is not None:
+            env["CONTENT_LENGTH"] = cl
+        
+        if req.conn.ssl_env:
+            env.update(req.conn.ssl_env)
+        
+        return env
+
+
+class WSGIGateway_u0(WSGIGateway):
+    
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version"""
+        req = self.req
+        env_10 = WSGIGateway_10.get_environ(self)
+        env = dict([(k.decode('ISO-8859-1'), v) for k, v in env_10.iteritems()])
+        env[u'wsgi.version'] = ('u', 0),
+        
+        # Request-URI
+        env.setdefault(u'wsgi.url_encoding', u'utf-8')
+        try:
+            for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
+                env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding'])
+        except UnicodeDecodeError:
+            # Fall back to latin 1 so apps can transcode if needed.
+            env[u'wsgi.url_encoding'] = u'ISO-8859-1'
+            for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
+                env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding'])
+        
+        for k, v in sorted(env.items()):
+            if isinstance(v, str) and k not in ('REQUEST_URI', 'wsgi.input'):
+                env[k] = v.decode('ISO-8859-1')
+        
+        return env
+
+wsgi_gateways = {
+    (1, 0): WSGIGateway_10,
+    ('u', 0): WSGIGateway_u0,
+}
+
+class WSGIPathInfoDispatcher(object):
+    """A WSGI dispatcher for dispatch based on the PATH_INFO.
+    
+    apps: a dict or list of (path_prefix, app) pairs.
+    """
+    
+    def __init__(self, apps):
+        try:
+            apps = apps.items()
+        except AttributeError:
+            pass
+        
+        # Sort the apps by len(path), descending
+        apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0])))
+        apps.reverse()
+        
+        # The path_prefix strings must start, but not end, with a slash.
+        # Use "" instead of "/".
+        self.apps = [(p.rstrip("/"), a) for p, a in apps]
+    
+    def __call__(self, environ, start_response):
+        path = environ["PATH_INFO"] or "/"
+        for p, app in self.apps:
+            # The apps list should be sorted by length, descending.
+            if path.startswith(p + "/") or path == p:
+                environ = environ.copy()
+                environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
+                environ["PATH_INFO"] = path[len(p):]
+                return app(environ, start_response)
+        
+        start_response('404 Not Found', [('Content-Type', 'text/plain'),
+                                         ('Content-Length', '0')])
+        return ['']
+
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.