Commits

Robert Brewer committed da5ffdf

sync with python3, including separation of InternalRedirect handling and Exception trapping to WSGI middleware.

Comments (0)

Files changed (13)

cherrypy/_cperror.py

     
     def __init__(self, path, query_string=""):
         import cherrypy
-        request = cherrypy.serving.request
+        self.request = cherrypy.serving.request
         
         self.query_string = query_string
         if "?" in path:
         #  1. a URL relative to root (e.g. "/dummy")
         #  2. a URL relative to the current path
         # Note that any query string will be discarded.
-        path = _urljoin(request.path_info, path)
+        path = _urljoin(self.request.path_info, path)
         
         # Set a 'path' member attribute so that code which traps this
         # error can have access to it.

cherrypy/_cpreqbody.py

     '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, has_trailers=False):

cherrypy/_cprequest.py

     
     query_string_encoding = 'utf8'
     query_string_encoding__doc = """
-    The encoding expected for query string arguments. If a query string
-    is provided that cannot be decoded with this encoding, 404 is raised
-    (since technically it's a different URI). If you want arbitrary
-    encodings to not error, set this to 'Latin-1'; you can then encode
-    back to bytes and re-decode to whatever encoding you like later.
+    The encoding expected for query string arguments after % HEX HEX decoding).
+    If a query string is provided that cannot be decoded with this encoding,
+    404 is raised (since technically it's a different URI). If you want
+    arbitrary encodings to not error, set this to 'Latin-1'; you can then
+    encode back to bytes and re-decode to whatever encoding you like later.
     """
     
     protocol = (1, 1)
     
     def get_resource(self, path):
         """Call a dispatcher (which sets self.handler and .config). (Core)"""
-        dispatch = self.dispatch
         # First, see if there is a custom dispatch at this URI. Custom
         # dispatchers can only be specified in app.config, not in _cp_config
         # (since custom dispatchers may not even have an app.root).
-        trail = path or "/"
-        while trail:
-            nodeconf = self.app.config.get(trail, {})
-            
-            d = nodeconf.get("request.dispatch")
-            if d:
-                dispatch = d
-                break
-            
-            lastslash = trail.rfind("/")
-            if lastslash == -1:
-                break
-            elif lastslash == 0 and trail != "/":
-                trail = "/"
-            else:
-                trail = trail[:lastslash]
+        dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
         
         # dispatch() should set self.handler and self.config
         dispatch(path)

cherrypy/_cpserver.py

     ssl_private_key = None
     ssl_module = 'pyopenssl'
     nodelay = True
-    wsgi_version = (1, 0)
+    wsgi_version = (1, 1)
     
     def __init__(self):
         self.bus = cherrypy.engine

cherrypy/_cptree.py

         # Handle namespaces specified in config.
         self.namespaces(self.config.get("/", {}))
     
+    def find_config(self, path, key, default=None):
+        """Return the most-specific value for key along path, or default."""
+        trail = path or "/"
+        while trail:
+            nodeconf = self.config.get(trail, {})
+            
+            if key in nodeconf:
+                return nodeconf[key]
+            
+            lastslash = trail.rfind("/")
+            if lastslash == -1:
+                break
+            elif lastslash == 0 and trail != "/":
+                trail = "/"
+            else:
+                trail = trail[:lastslash]
+        
+        return default
+    
     def get_serving(self, local, remote, scheme, sproto):
         """Create and return a Request and Response object."""
         req = self.request_class(local, remote, scheme, sproto)
         # If you're calling this, then you're probably setting SCRIPT_NAME
         # to '' (some WSGI servers always set SCRIPT_NAME to '').
         # Try to look up the app using the full path.
-        env11 = environ
-        if environ.get(u'wsgi.version') == (1, 1):
-            env11 = _cpwsgi.downgrade_wsgi_11_to_10(environ)
-        path = httputil.urljoin(env11.get('SCRIPT_NAME', ''),
-                                env11.get('PATH_INFO', ''))
+        env1x = environ
+        if environ.get(u'wsgi.version') == (u'u', 0):
+            env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
+        path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
+                                env1x.get('PATH_INFO', ''))
         sn = self.script_name(path or "/")
         if sn is None:
             start_response('404 Not Found', [])
         
         # Correct the SCRIPT_NAME and PATH_INFO environ entries.
         environ = environ.copy()
-        if environ.get(u'wsgi.version') == (1, 1):
-            # Python 2/WSGI 1.1: all strings MUST be of type unicode
+        if environ.get(u'wsgi.version') == (u'u', 0):
+            # Python 2/WSGI u.0: all strings MUST be of type unicode
             enc = environ[u'wsgi.url_encoding']
             environ[u'SCRIPT_NAME'] = sn.decode(enc)
             environ[u'PATH_INFO'] = path[len(sn.rstrip("/")):].decode(enc)
         else:
-            # Python 2/WSGI 1.0: all strings MUST be of type str
+            # Python 2/WSGI 1.x: all strings MUST be of type str
             environ['SCRIPT_NAME'] = sn
             environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
         return app(environ, start_response)

cherrypy/_cpwsgi.py

 from cherrypy.lib import httputil
 
 
-def downgrade_wsgi_u0_to_10(environ):
-    """Return a new environ dict for WSGI 1.0 from the given WSGI u.0 environ."""
-    env10 = {}
+def downgrade_wsgi_ux_to_1x(environ):
+    """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
+    env1x = {}
     
     url_encoding = environ[u'wsgi.url_encoding']
     for k, v in environ.items():
             v = v.encode(url_encoding)
         elif isinstance(v, unicode):
             v = v.encode('ISO-8859-1')
-        env10[k.encode('ISO-8859-1')] = v
+        env1x[k.encode('ISO-8859-1')] = v
     
-    return env10
+    return env1x
 
 
 class VirtualHost(object):
         return nextapp(environ, start_response)
 
 
+class InternalRedirector(object):
+    """WSGI middleware that handles raised cherrypy.InternalRedirect."""
+    
+    def __init__(self, nextapp, recursive=False):
+        self.nextapp = nextapp
+        self.recursive = recursive
+    
+    def __call__(self, environ, start_response):
+        redirections = []
+        while True:
+            environ = environ.copy()
+            try:
+                return self.nextapp(environ, start_response)
+            except _cherrypy.InternalRedirect, ir:
+                sn = environ.get('SCRIPT_NAME', '')
+                path = environ.get('PATH_INFO', '')
+                qs = environ.get('QUERY_STRING', '')
+                
+                # Add the *previous* path_info + qs to redirections.
+                old_uri = sn + path
+                if qs:
+                    old_uri += "?" + qs
+                redirections.append(old_uri)
+                
+                if not self.recursive:
+                    # Check to see if the new URI has been redirected to already
+                    new_uri = sn + ir.path
+                    if ir.query_string:
+                        new_uri += "?" + ir.query_string
+                    if new_uri in redirections:
+                        req.close()
+                        raise RuntimeError("InternalRedirector visited the "
+                                           "same URL twice: %r" % new_uri)
+                
+                # Munge the environment and try again.
+                environ['REQUEST_METHOD'] = "GET"
+                environ['PATH_INFO'] = ir.path
+                environ['QUERY_STRING'] = ir.query_string
+                environ['wsgi.input'] = StringIO()
+                environ['CONTENT_LENGTH'] = "0"
+                environ['cherrypy.previous_request'] = ir.request
 
-#                           WSGI-to-CP Adapter                           #
 
+class ExceptionTrapper(object):
+    
+    def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
+        self.nextapp = nextapp
+        self.throws = throws
+    
+    def __call__(self, environ, start_response):
+        return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
 
-class AppResponse(object):
+
+class _TrappedResponse(object):
     
-    throws = (KeyboardInterrupt, SystemExit)
-    request = None
+    response = iter([])
     
-    def __init__(self, environ, start_response, cpapp, recursive=False):
-        self.redirections = []
-        self.recursive = recursive
-        if environ.get(u'wsgi.version') == (u'u', 0):
-            environ = downgrade_wsgi_u0_to_10(environ)
+    def __init__(self, nextapp, environ, start_response, throws):
+        self.nextapp = nextapp
         self.environ = environ
         self.start_response = start_response
-        self.cpapp = cpapp
-        self.setapp()
+        self.throws = throws
+        self.started_response = False
+        self.response = self.trap(self.nextapp, self.environ, self.start_response)
+        self.iter_response = iter(self.response)
     
-    def setapp(self):
+    def __iter__(self):
+        self.started_response = True
+        return self
+    
+    def next(self):
+        return self.trap(self.iter_response.next)
+    
+    def close(self):
+        if hasattr(self.response, 'close'):
+            self.response.close()
+    
+    def trap(self, func, *args, **kwargs):
         try:
-            self.request = self.get_request()
-            s, h, b = self.get_response()
-            self.iter_response = iter(b)
-            self.write = self.start_response(s, h)
+            return func(*args, **kwargs)
         except self.throws:
-            self.close()
             raise
-        except _cherrypy.InternalRedirect, ir:
-            self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
-            self.close()
-            self.iredirect(ir.path, ir.query_string)
-            return
+        except StopIteration:
+            raise
         except:
-            if getattr(self.request, "throw_errors", False):
-                self.close()
-                raise
-            
             tb = _cperror.format_exc()
+            #print('trapped (started %s):' % self.started_response, tb)
             _cherrypy.log(tb, severity=40)
-            if not getattr(self.request, "show_tracebacks", True):
+            if not _cherrypy.request.show_tracebacks:
                 tb = ""
             s, h, b = _cperror.bare_error(tb)
-            self.iter_response = iter(b)
+            if self.started_response:
+                # Empty our iterable (so future calls raise StopIteration)
+                self.iter_response = iter([])
+            else:
+                self.iter_response = iter(b)
             
             try:
                 self.start_response(s, h, _sys.exc_info())
                 # back to the server or gateway."
                 # But we still log and call close() to clean up ourselves.
                 _cherrypy.log(traceback=True, severity=40)
-                self.close()
                 raise
+            
+            if self.started_response:
+                return "".join(b)
+            else:
+                return b
+
+
+#                           WSGI-to-CP Adapter                           #
+
+
+class AppResponse(object):
+    """WSGI response iterable for CherryPy applications."""
     
-    def iredirect(self, path, query_string):
-        """Doctor self.environ and perform an internal redirect.
-        
-        When cherrypy.InternalRedirect is raised, this method is called.
-        It rewrites the WSGI environ using the new path and query_string,
-        and calls a new CherryPy Request object. Because the wsgi.input
-        stream may have already been consumed by the next application,
-        the redirected call will always be of HTTP method "GET"; therefore,
-        any params must be passed in the query_string argument, which is
-        formed from InternalRedirect.query_string when using that exception.
-        If you need something more complicated, make and raise your own
-        exception and write your own AppResponse subclass to trap it. ;)
-        
-        It would be a bad idea to redirect after you've already yielded
-        response content, although an enterprising soul could choose
-        to abuse this.
-        """
-        env = self.environ
-        if not self.recursive:
-            sn = env.get('SCRIPT_NAME', '')
-            qs = query_string
-            if qs:
-                qs = "?" + qs
-            if sn + path + qs in self.redirections:
-                raise RuntimeError("InternalRedirector visited the "
-                                   "same URL twice: %r + %r + %r" %
-                                   (sn, path, qs))
-            else:
-                # Add the *previous* path_info + qs to redirections.
-                p = env.get('PATH_INFO', '')
-                qs = env.get('QUERY_STRING', '')
-                if qs:
-                    qs = "?" + qs
-                self.redirections.append(sn + p + qs)
-        
-        # Munge environment and try again.
-        env['REQUEST_METHOD'] = "GET"
-        env['PATH_INFO'] = path
-        env['QUERY_STRING'] = query_string
-        env['wsgi.input'] = StringIO()
-        env['CONTENT_LENGTH'] = "0"
-        
-        self.setapp()
+    def __init__(self, environ, start_response, cpapp):
+        if environ.get(u'wsgi.version') == (u'u', 0):
+            environ = downgrade_wsgi_ux_to_1x(environ)
+        self.environ = environ
+        self.cpapp = cpapp
+        try:
+            self.run()
+        except:
+            self.close()
+            raise
+        r = _cherrypy.serving.response
+        self.iter_response = iter(r.body)
+        self.write = start_response(r.output_status, r.header_list)
     
     def __iter__(self):
         return self
     
     def next(self):
-        try:
-            chunk = self.iter_response.next()
-            # WSGI 1.x requires all response data to be of type "str".
-            # This coercion should not take any time at all if chunk is
-            # already of type "str".
-            # If it's unicode, it could be a big performance hit (x ~500).
-            if not isinstance(chunk, str):
-                chunk = unicode(chunk).encode("ISO-8859-1")
-            return chunk
-        except self.throws:
-            self.close()
-            raise
-        except _cherrypy.InternalRedirect, ir:
-            self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
-            self.close()
-            self.iredirect(ir.path, ir.query_string)
-        except StopIteration:
-            raise
-        except:
-            if getattr(self.request, "throw_errors", False):
-                self.close()
-                raise
-            
-            tb = _cperror.format_exc()
-            _cherrypy.log(tb, severity=40)
-            if not getattr(self.request, "show_tracebacks", True):
-                tb = ""
-            s, h, b = _cperror.bare_error(tb)
-            # Empty our iterable (so future calls raise StopIteration)
-            self.iter_response = iter([])
-            
-            try:
-                self.start_response(s, h, _sys.exc_info())
-            except:
-                # "The application must not trap any exceptions raised by
-                # start_response, if it called start_response with exc_info.
-                # Instead, it should allow such exceptions to propagate
-                # back to the server or gateway."
-                # But we still log and call close() to clean up ourselves.
-                _cherrypy.log(traceback=True, severity=40)
-                self.close()
-                raise
-            
-            return "".join(b)
+        return self.iter_response.next()
     
     def close(self):
         """Close and de-reference the current request and response. (Core)"""
         self.cpapp.release_serving()
     
-    def get_response(self):
-        """Run self.request and return its response."""
-        meth = self.environ['REQUEST_METHOD']
-        path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
-                                self.environ.get('PATH_INFO', ''))
-        qs = self.environ.get('QUERY_STRING', '')
-        rproto = self.environ.get('SERVER_PROTOCOL')
-        headers = self.translate_headers(self.environ)
-        rfile = self.environ['wsgi.input']
-        response = self.request.run(meth, path, qs, rproto, headers, rfile)
-        return response.output_status, response.header_list, response.body
-    
-    def get_request(self):
+    def run(self):
         """Create a Request object using environ."""
         env = self.environ.get
         
         request.multiprocess = self.environ['wsgi.multiprocess']
         request.wsgi_environ = self.environ
         request.prev = env('cherrypy.previous_request', None)
-        return request
+        
+        meth = self.environ['REQUEST_METHOD']
+        
+        path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
+                                self.environ.get('PATH_INFO', ''))
+        qs = self.environ.get('QUERY_STRING', '')
+        rproto = self.environ.get('SERVER_PROTOCOL')
+        headers = self.translate_headers(self.environ)
+        rfile = self.environ['wsgi.input']
+        request.run(meth, path, qs, rproto, headers, rfile)
     
     headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
                    'CONTENT_LENGTH': 'Content-Length',
         named WSGI callable (from the pipeline) as keyword arguments.
     """
     
-    pipeline = []
+    pipeline = [('ExceptionTrapper', ExceptionTrapper),
+                ('InternalRedirector', InternalRedirector),
+                ]
     head = None
     config = {}
     

cherrypy/_cpwsgi_server.py

                 self.server_adapter.ssl_certificate,
                 self.server_adapter.ssl_private_key,
                 self.server_adapter.ssl_certificate_chain)
-
-

cherrypy/lib/cptools.py

     
     if debug:
         cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
-                     (request.is_index, missing, extra, request.path_info),
+                     (request.is_index, missing, extra, pi),
                      'TOOLS.TRAILING_SLASH')
     if request.is_index is True:
         if missing:

cherrypy/test/test.py

     Usage:
         test.py --help --server=* --host=%s --port=%s --1.0 --ssl --cover
             --basedir=path --profile --validate --conquer --dumb --tests**
-        
-    """ % (self.__class__.host, self.__class__.port))
+    """ % (self.__class__.supervisor_options['host'],
+           self.__class__.supervisor_options['port']))
         print('    * servers:')
-        for name in self.available_servers.items():
+        for name in self.available_servers:
             if name == self.default_server:
                 print('        --server=%s (default)' % name)
             else:
                 print('        --server=%s' % name)
         
         print("""
-    
     --host=<name or IP addr>: use a host other than the default (%s).
         Not yet available with mod_python servers.
     --port=<int>: use a port other than the default (%s).
     --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.
-    """ % (self.__class__.host, self.__class__.port))
+    """ % (self.__class__.supervisor_options['host'],
+           self.__class__.supervisor_options['port']))
         
         print('    ** tests:')
         for name in self.available_tests:

cherrypy/test/test_conn.py

         self.assertEqual(response.status, 200)
         self.body = response.read()
         self.assertBody(pov)
+
         
         # Make another request on the same socket,
         # but timeout on the headers
         
         # 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"
+                "Content-Type: application/json\r\n"
                 "\r\n")
         conn.putrequest("POST", "/upload", skip_host=True)
         conn.putheader("Host", self.HOST)

cherrypy/test/test_encoding.py

         
         def reqparams(self, *args, **kwargs):
             return ', '.join([": ".join((k, v)).encode('utf8')
-                             for k, v in cherrypy.request.params.items()])
+                              for k, v in cherrypy.request.params.items()])
         reqparams.exposed = True
     
     class GZIP:
     class Decode:
         def extra_charset(self, *args, **kwargs):
             return ', '.join([": ".join((k, v)).encode('utf8')
-                             for k, v in cherrypy.request.params.items()])
+                              for k, v in cherrypy.request.params.items()])
         extra_charset.exposed = True
         extra_charset._cp_config = {
             'tools.decode.on': True,
         
         def force_charset(self, *args, **kwargs):
             return ', '.join([": ".join((k, v)).encode('utf8')
-                             for k, v in cherrypy.request.params.items()])
+                              for k, v in cherrypy.request.params.items()])
         force_charset.exposed = True
         force_charset._cp_config = {
             'tools.decode.on': True,
         # Encoded utf8 query strings MUST be parsed correctly.
         # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
         self.getPage("/reqparams?q=%C2%A3")
+        # The return value will be encoded as utf8.
         self.assertBody("q: \xc2\xa3")
         
         # Query strings that are incorrectly encoded MUST raise 404.

cherrypy/test/test_tools.py

         pipe._cp_config = {'hooks.before_request_body': pipe_body}
         
         # Multiple decorators; include kwargs just for fun.
-        # Note that encode must run before gzip.
+        # Note that rotator must run before gzip.
         def decorated_euro(self, *vpath):
             yield u"Hello,"
             yield u"world"

cherrypy/wsgiserver/__init__.py

         
         buf = [self.server.protocol + " " + self.status + CRLF]
         for k, v in self.outheaders:
-            buf.append(k + ": " + v + "\r\n")
+            buf.append(k + ": " + v + CRLF)
         buf.append(CRLF)
         self.conn.wfile.sendall("".join(buf))
 
         except NoSSLError:
             if req and not req.sent_headers:
                 # Unwrap our wfile
-                req.conn.wfile = CP_fileobject(self.socket._sock, "wb", -1)
+                self.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.")
 
 class CherryPyWSGIServer(HTTPServer):
     
-    wsgi_version = (1, 0)
+    wsgi_version = (1, 1)
     
     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
                  max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
         start_response('404 Not Found', [('Content-Type', 'text/plain'),
                                          ('Content-Length', '0')])
         return ['']
-