Commits

Robert Brewer committed 2e9393f

Merged the 'gateways' branch into trunk.

  • Participants
  • Parent commits 5d8a18f

Comments (0)

Files changed (14)

File 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)
+
+
+class CPHTTPServer(wsgiserver.HTTPServer):
+    """Wrapper for wsgiserver.HTTPServer.
+    
+    wsgiserver has been designed to not reference CherryPy in any way,
+    so that it can be used in other frameworks and applications.
+    Therefore, we wrap it here, so we can apply some attributes
+    from config -> cherrypy.server -> HTTPServer.
+    """
+    
+    def __init__(self, server_adapter=cherrypy.server):
+        self.server_adapter = server_adapter
+        
+        server_name = (self.server_adapter.socket_host or
+                       self.server_adapter.socket_file or
+                       None)
+        
+        wsgiserver.HTTPServer __init__(
+            self, server_adapter.bind_addr, NativeGateway,
+            minthreads=server_adapter.thread_pool,
+            maxthreads=server_adapter.thread_pool_max,
+            server_name=server_name):
+        
+        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
+        self.request_queue_size = self.server_adapter.socket_queue_size
+        self.timeout = self.server_adapter.socket_timeout
+        self.shutdown_timeout = self.server_adapter.shutdown_timeout
+        self.protocol = self.server_adapter.protocol_version
+        self.nodelay = self.server_adapter.nodelay
+        
+        ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
+        if self.server_adapter.ssl_context:
+            adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain)
+            self.ssl_adapter.context = self.server_adapter.ssl_context
+        elif self.server_adapter.ssl_certificate:
+            adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain)
+
+

File 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
+

File 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']
-    for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
-        env10[str(key)] = environ[key].encode(enc)
-    
+    url_encoding = environ[u'wsgi.url_encoding']
     for k, v in environ.items():
         if k in [u'PATH_INFO', u'SCRIPT_NAME', u'QUERY_STRING']:
-            continue
-        if isinstance(v, unicode) and k not in [u'REQUEST_URI', u'wsgi.input']:
+            v = v.encode(url_encoding)
+        elif isinstance(v, unicode):
             v = v.encode('ISO-8859-1')
         env10[k.encode('ISO-8859-1')] = v
     
     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'u', 0):
+            environ = downgrade_wsgi_u0_to_10(environ)
         self.environ = environ
         self.start_response = start_response
         self.cpapp = cpapp

File 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
-        
+        ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
         if self.server_adapter.ssl_context:
-            adapter_class = self.get_ssl_adapter_class()
-            s.ssl_adapter = adapter_class(self.server_adapter.ssl_certificate,
-                                          self.server_adapter.ssl_private_key,
-                                          self.server_adapter.ssl_certificate_chain)
-            s.ssl_adapter.context = self.server_adapter.ssl_context
+            adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain)
+            self.ssl_adapter.context = self.server_adapter.ssl_context
         elif self.server_adapter.ssl_certificate:
-            adapter_class = self.get_ssl_adapter_class()
-            s.ssl_adapter = adapter_class(self.server_adapter.ssl_certificate,
-                                          self.server_adapter.ssl_private_key,
-                                          self.server_adapter.ssl_certificate_chain)
-    
-    def get_ssl_adapter_class(self):
-        adname = (self.server_adapter.ssl_module or 'pyopenssl').lower()
-        adapter = ssl_adapters[adname]
-        if isinstance(adapter, basestring):
-            last_dot = adapter.rfind(".")
-            attr_name = adapter[last_dot + 1:]
-            mod_path = adapter[:last_dot]
-            
-            try:
-                mod = sys.modules[mod_path]
-                if mod is None:
-                    raise KeyError()
-            except KeyError:
-                # The last [''] is important.
-                mod = __import__(mod_path, globals(), locals(), [''])
-            
-            # Let an AttributeError propagate outward.
-            try:
-                adapter = getattr(mod, attr_name)
-            except AttributeError:
-                raise AttributeError("'%s' object has no attribute '%s'"
-                                     % (mod_path, attr_name))
-        
-        return adapter
+            adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain)
 
-# These may either be wsgiserver.SSLAdapter subclasses or the string names
-# of such classes (in which case they will be lazily loaded).
-ssl_adapters = {
-    'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
-    'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
-    }
 

File cherrypy/scaffold/site.conf

 server.socket_host: "0.0.0.0"
 server.socket_port: 8088
 
+# Uncomment the following lines to run on HTTPS at the same time
+#server.2.socket_host: "0.0.0.0"
+#server.2.socket_port: 8433
+#server.2.ssl_certificate: '../test/test.pem'
+#server.2.ssl_private_key: '../test/test.pem'
+
 tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")

File cherrypy/test/helper.py

 CPTestRunner = webtest.TerseTestRunner(verbosity=2)
 
 
-def run_test_suite(moduleNames, conf, server):
-    """Run the given test modules using the given server and [global] conf.
+def run_test_suite(moduleNames, conf, supervisor):
+    """Run the given test modules using the given supervisor and [global] conf.
     
-    The 'server' arg should be an object with 'start' and 'stop' methods.
+    The 'supervisor' arg should be an object with 'start' and 'stop' methods.
     See test/test.py.
     """
     # The Pybots automatic testing system needs the suite to exit
     for testmod in moduleNames:
         cherrypy.config.reset()
         cherrypy.config.update(conf)
-        setup_client()
+        setup_client(supervisor)
         
         if '.' in testmod:
             package, test_name = testmod.rsplit('.', 1)
         suite = CPTestLoader.loadTestsFromName(testmod)
         
         setup = getattr(m, "setup_server", None)
-        if setup: server.start(testmod)
+        if setup: supervisor.start(testmod)
         try:
             result = CPTestRunner.run(suite)
             test_success &= result.wasSuccessful()
         finally:
-            if setup: server.stop()
+            if setup: supervisor.stop()
     
     if test_success:
         return 0
     else:
         return 1
 
-def setup_client():
+def setup_client(supervisor):
     """Set up the WebCase classes to match the server's socket settings."""
     webtest.WebCase.PORT = cherrypy.server.socket_port
     webtest.WebCase.HOST = cherrypy.server.socket_host
     """Run __main__ as a test module, with webtest debugging."""
     # Comment me out to see ENGINE messages etc. when running a test standalone.
     cherrypy.config.update({'environment': "test_suite"})
+    cherrypy.server.socket_host = '127.0.0.1'
     
-    cherrypy.server.socket_host = '127.0.0.1'
-    setup_client()
-    
-    from cherrypy.test.test import LocalServer
-    server = LocalServer(cherrypy.server.socket_host, cherrypy.server.socket_port,
-                         False, False, False)
-    server.start('__main__')
+    from cherrypy.test.test import LocalWSGISupervisor
+    supervisor = LocalWSGISupervisor(host=cherrypy.server.socket_host,
+                                     port=cherrypy.server.socket_port)
+    setup_client(supervisor)
+    supervisor.start('__main__')
     try:
         return webtest.main()
     finally:
-        server.stop()
+        supervisor.stop()
 
 
 

File cherrypy/test/modfcgid.py

 FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
 """
 
-class ServerControl(test.LocalServer):
+class ModFCGISupervisor(test.LocalSupervisor):
     
+    using_apache = True
+    using_wsgi = True
     template = conf_fcgid
     
     def __str__(self):

File cherrypy/test/modpy.py

 PythonDebug On
 """
 
-class ServerControl(object):
+class ModPythonSupervisor(test.Supervisor):
     
-    def __init__(self, host, port, template):
-        self.host = host
-        self.port = port
-        self.template = template
+    using_apache = True
+    using_wsgi = False
+    template = None
     
     def __str__(self):
         return "ModPython Server on %s:%s" % (self.host, self.port)

File cherrypy/test/modwsgi.py

 """
 
 
-class ServerControl(object):
+class ModWSGISupervisor(test.Supervisor):
     """Server Controller for ModWSGI and CherryPy."""
     
-    def __init__(self, host, port, template=conf_modwsgi):
-        self.host = host
-        self.port = port
-        self.template = template
+    using_apache = True
+    using_wsgi = True
+    template=conf_modwsgi
     
     def __str__(self):
         return "ModWSGI Server on %s:%s" % (self.host, self.port)

File cherrypy/test/test.py

 class TestHarness(object):
     """A test harness for the CherryPy framework and CherryPy applications."""
     
-    def __init__(self, tests=None, server=None, protocol="HTTP/1.1",
-                 port=8000, scheme="http", interactive=True, host='127.0.0.1'):
+    def __init__(self, supervisor, tests, interactive=True):
         """Constructor to populate the TestHarness instance.
         
         tests should be a list of module names (strings).
         """
-        self.tests = tests or []
-        self.server = server
-        self.protocol = protocol
-        self.port = port
-        self.host = host
-        self.scheme = scheme
+        self.supervisor = supervisor
+        self.tests = tests
         self.interactive = interactive
     
     def run(self, conf=None):
         v = sys.version.split()[0]
         print("Python version used to run this test script: %s" % v)
         print("CherryPy version: %s" % cherrypy.__version__)
-        if self.scheme == "https":
+        if self.supervisor.scheme == "https":
             ssl = " (ssl)"
         else:
             ssl = ""
-        print("HTTP server version: %s%s" % (self.protocol, ssl))
+        print("HTTP server version: %s%s" % (self.supervisor.protocol, ssl))
         print("PID: %s" % os.getpid())
         print("")
         
+        cherrypy.server.using_apache = self.supervisor.using_apache
+        cherrypy.server.using_wsgi = self.supervisor.using_wsgi
+        
         if isinstance(conf, basestring):
             parser = cherrypy.config._Parser()
             conf = parser.dict_from_file(conf).get('global', {})
         else:
             conf = conf or {}
         baseconf = conf.copy()
-        baseconf.update({'server.socket_host': self.host,
-                         'server.socket_port': self.port,
-                         'server.protocol_version': self.protocol,
+        baseconf.update({'server.socket_host': self.supervisor.host,
+                         'server.socket_port': self.supervisor.port,
+                         'server.protocol_version': self.supervisor.protocol,
                          'environment': "test_suite",
                          })
-        if self.scheme == "https":
+        if self.supervisor.scheme == "https":
             baseconf['server.ssl_certificate'] = serverpem
             baseconf['server.ssl_private_key'] = serverpem
         
         # and we wouldn't be able to globally override the port anymore.
         from cherrypy.test import helper, webtest
         webtest.WebCase.interactive = self.interactive
-        if self.scheme == "https":
+        if self.supervisor.scheme == "https":
             webtest.WebCase.HTTP_CONN = HTTPSConnection
         print("")
-        print("Running tests: %s" % self.server)
+        print("Running tests: %s" % self.supervisor)
         
-        return helper.run_test_suite(self.tests, baseconf, self.server)
+        return helper.run_test_suite(self.tests, baseconf, self.supervisor)
 
 
-class LocalServer(object):
-    """Server Controller for the builtin WSGI server."""
+class Supervisor(object):
+    """Base class for modeling and controlling servers during testing."""
     
-    def __init__(self, host, port, profile, validate, conquer):
-        self.host = host
-        self.port = port
-        self.profile = profile
-        self.validate = validate
-        self.conquer = conquer
+    def __init__(self, **kwargs):
+        for k, v in kwargs.iteritems():
+            setattr(self, k, v)
+
+
+class LocalSupervisor(Supervisor):
+    """Base class for modeling/controlling servers which run in the same process.
     
-    def __str__(self):
-        return "Builtin WSGI Server on %s:%s" % (self.host, self.port)
+    When the server side runs in a different process, start/stop can dump all
+    state between each test module easily. When the server side runs in the
+    same process as the client, however, we have to do a bit more work to ensure
+    config and mounted apps are reset between tests.
+    """
+    
+    using_apache = False
+    using_wsgi = False
+    
+    def __init__(self, **kwargs):
+        for k, v in kwargs.iteritems():
+            setattr(self, k, v)
+        
+        import cherrypy
+        cherrypy.server.httpserver = self.httpserver_class
+        
+        engine = cherrypy.engine
+        if hasattr(engine, "signal_handler"):
+            engine.signal_handler.subscribe()
+        if hasattr(engine, "console_control_handler"):
+            engine.console_control_handler.subscribe()
+        #engine.subscribe('log', lambda msg, level: sys.stderr.write(msg + os.linesep))
     
     def start(self, modulename=None):
         """Load and start the HTTP server."""
         
         cherrypy.engine.start()
         
-        # The setup functions probably mounted new apps.
-        # Tell our server about them.
         self.sync_apps()
     
+    def sync_apps(self):
+        """Tell the server about any apps which the setup functions mounted."""
+        pass
+    
     def stop(self):
         if self.teardown:
             self.teardown()
         import cherrypy
         cherrypy.engine.exit()
+
+
+class NativeServerSupervisor(LocalSupervisor):
+    """Server supervisor for the builtin HTTP server."""
+    
+    httpserver_class = "cherrypy._cpnative_server.CPHTTPServer"
+    using_apache = False
+    using_wsgi = False
+    
+    def __str__(self):
+        return "Builtin HTTP Server on %s:%s" % (self.host, self.port)
+
+
+class LocalWSGISupervisor(LocalSupervisor):
+    """Server supervisor for the builtin WSGI server."""
+    
+    httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer"
+    using_apache = False
+    using_wsgi = True
+    
+    def __str__(self):
+        return "Builtin WSGI Server on %s:%s" % (self.host, self.port)
     
     def sync_apps(self):
         """Hook a new WSGI app into the origin server."""
         return app
 
 
+def get_cpmodpy_supervisor(**options):
+    from cherrypy.test import modpy
+    sup = modpy.ModPythonSupervisor(**options)
+    sup.template = modpy.conf_cpmodpy
+    return sup
+
+def get_modpygw_supervisor(**options):
+    from cherrypy.test import modpy
+    sup = modpy.ModPythonSupervisor(**options)
+    sup.template = modpy.conf_modpython_gateway
+    sup.using_wsgi = True
+    return sup
+
+def get_modwsgi_supervisor(**options):
+    from cherrypy.test import modwsgi
+    return modwsgi.ModWSGISupervisor(**options)
+
+def get_modfcgid_supervisor(**options):
+    from cherrypy.test import modfcgid
+    return modfcgid.ModFCGISupervisor(**options)
+
+def get_wsgi_u_supervisor(**options):
+    import cherrypy
+    cherrypy.server.wsgi_version = ('u', 0)
+    return LocalWSGISupervisor(**options)
+
+
 class CommandLineParser(object):
-    available_servers = {'wsgi': "cherrypy._cpwsgi.CPWSGIServer",
-                         'cpmodpy': "cpmodpy",
-                         'modpygw': "modpygw",
-                         'modwsgi': "modwsgi",
-                         'modfcgid': "modfcgid",
+    available_servers = {'wsgi': LocalWSGISupervisor,
+                         'wsgi_u': get_wsgi_u_supervisor,
+                         'native': NativeServerSupervisor,
+                         'cpmodpy': get_cpmodpy_supervisor,
+                         'modpygw': get_modpygw_supervisor,
+                         'modwsgi': get_modwsgi_supervisor,
+                         'modfcgid': get_modfcgid_supervisor,
                          }
     default_server = "wsgi"
-    scheme = "http"
-    protocol = "HTTP/1.1"
-    port = 8080
-    host = '127.0.0.1'
+    
+    supervisor_factory = None
+    supervisor_options = {
+        'scheme': 'http',
+        'protocol': "HTTP/1.1",
+        'port': 8080,
+        'host': '127.0.0.1',
+        'profile': False,
+        'validate': False,
+        'conquer': False,
+        }
+    
     cover = False
-    profile = False
-    validate = False
-    conquer = False
-    server = None
     basedir = None
     interactive = True
     
             set of args if you like.
         """
         self.available_tests = available_tests
+        self.supervisor_options = self.supervisor_options.copy()
         
         longopts = self.longopts[:]
         longopts.extend(self.available_tests)
             elif o == "--cover":
                 self.cover = True
             elif o == "--profile":
-                self.profile = True
+                self.supervisor_options['profile'] = True
             elif o == "--validate":
-                self.validate = True
+                self.supervisor_options['validate'] = True
             elif o == "--conquer":
-                self.conquer = True
+                self.supervisor_options['conquer'] = True
             elif o == "--dumb":
                 self.interactive = False
             elif o == "--1.0":
-                self.protocol = "HTTP/1.0"
+                self.supervisor_options['protocol'] = "HTTP/1.0"
             elif o == "--ssl":
-                self.scheme = "https"
+                self.supervisor_options['scheme'] = "https"
             elif o == "--basedir":
                 self.basedir = a
             elif o == "--port":
-                self.port = int(a)
+                self.supervisor_options['port'] = int(a)
             elif o == "--host":
-                self.host = a
+                self.supervisor_options['host'] = a
             elif o == "--server":
-                if a in self.available_servers:
-                    a = self.available_servers[a]
-                self.server = a
+                if a not in self.available_servers:
+                    print('Error: The --server argument must be one of %s.' %
+                          '|'.join(self.available_servers.keys()))
+                    sys.exit(2)
+                self.supervisor_factory = self.available_servers[a]
             else:
                 o = o[2:]
                 if o in self.available_tests and o not in self.tests:
                     self.tests.append(o)
         
         import cherrypy
-        if self.cover and self.profile:
+        if self.cover and self.supervisor_options['profile']:
             # Print error message and exit
             print('Error: you cannot run the profiler and the '
                    'coverage tool at the same time.')
             sys.exit(2)
         
-        if not self.server:
-            self.server = self.available_servers[self.default_server]
+        if not self.supervisor_factory:
+            self.supervisor_factory = self.available_servers[self.default_server]
         
         if not self.tests:
             self.tests = self.available_tests[:]
         
     """ % (self.__class__.host, self.__class__.port))
         print('    * servers:')
-        for name, val in self.available_servers.items():
+        for name in self.available_servers.items():
             if name == self.default_server:
-                print('        --server=%s: %s (default)' % (name, val))
+                print('        --server=%s (default)' % name)
             else:
-                print('        --server=%s: %s' % (name, val))
+                print('        --server=%s' % name)
         
         print("""
     
     --cover: turn on code-coverage tool.
     --basedir=path: display coverage stats for some path other than cherrypy.
     
-    --profile: turn on profiling tool.
+    --profile: turn on WSGI profiling tool.
     --validate: use wsgiref.validate (builtin in Python 2.5).
     --conquer: use wsgiconq (which uses pyconquer) to trace calls.
     --dumb: turn off the interactive output features.
         if self.cover:
             self.start_coverage()
         
-        import cherrypy
-        if self.server == 'cpmodpy':
-            from cherrypy.test import modpy
-            server = modpy.ServerControl(self.host, self.port,
-                                         modpy.conf_cpmodpy)
-            cherrypy.server.using_apache = True
-            cherrypy.server.using_wsgi = False
-        elif self.server == 'modpygw':
-            from cherrypy.test import modpy
-            server = modpy.ServerControl(self.host, self.port,
-                                         modpy.conf_modpython_gateway)
-            cherrypy.server.using_apache = True
-            cherrypy.server.using_wsgi = True
-        elif self.server == 'modwsgi':
-            from cherrypy.test import modwsgi
-            server = modwsgi.ServerControl(self.host, self.port)
-            cherrypy.server.using_apache = True
-            cherrypy.server.using_wsgi = True
-        elif self.server == 'modfcgid':
-            from cherrypy.test import modfcgid
-            server = modfcgid.ServerControl(self.host, self.port, self.profile,
-                                            self.validate, self.conquer)
-            cherrypy.server.using_apache = True
-            cherrypy.server.using_wsgi = True
-        else:
-            server = LocalServer(self.host, self.port, self.profile,
-                                 self.validate, self.conquer)
-            cherrypy.server.using_apache = False
-            cherrypy.server.using_wsgi = True
-            engine = cherrypy.engine
-            if hasattr(engine, "signal_handler"):
-                engine.signal_handler.subscribe()
-            if hasattr(engine, "console_control_handler"):
-                engine.console_control_handler.subscribe()
-            #engine.subscribe('log', lambda msg, level: sys.stderr.write(msg + os.linesep))
+        supervisor = self.supervisor_factory(**self.supervisor_options)
         
-        if cherrypy.server.using_apache and 'test_conn' in self.tests:
+        if supervisor.using_apache and 'test_conn' in self.tests:
             self.tests.remove('test_conn')
         
-        h = TestHarness(self.tests, server, self.protocol, self.port,
-                        self.scheme, self.interactive, self.host)
+        h = TestHarness(supervisor, self.tests, self.interactive)
         success = h.run(conf)
         
-        if self.profile:
+        if self.supervisor_options['profile']:
             print("")
             print("run /cherrypy/lib/profiler.py as a script to serve "
                    "profiling results on port 8080")

File 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):

File 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
         

File 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
-    
-    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.scheme = "http"
+        if self.server.ssl_adapter is not None:
+            self.scheme = "https"
+        self.inheaders = {}
+        
         self.status = ""
         self.outheaders = []
         self.sent_headers = 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 HTTPServer(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):