From 403eec1609e754980997ba3d188579999ff3911b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 23 Sep 2011 13:28:38 -0600 Subject: [PATCH] Content server: When WSGI embedding fix handling of empty URL --- src/cherrypy/_cpwsgi.py | 88 +++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/src/cherrypy/_cpwsgi.py b/src/cherrypy/_cpwsgi.py index b23822e568..b7442471f0 100644 --- a/src/cherrypy/_cpwsgi.py +++ b/src/cherrypy/_cpwsgi.py @@ -10,28 +10,28 @@ from cherrypy.lib import http as _http class VirtualHost(object): """Select a different WSGI application based on the Host header. - + This can be useful when running multiple sites within one CP server. It allows several domains to point to different applications. For example: - + root = Root() RootApp = cherrypy.Application(root) Domain2App = cherrypy.Application(root) SecureApp = cherrypy.Application(Secure()) - + vhost = cherrypy._cpwsgi.VirtualHost(RootApp, domains={'www.domain2.example': Domain2App, 'www.domain2.example:443': SecureApp, }) - + cherrypy.tree.graft(vhost) - + default: required. The default WSGI application. - + use_x_forwarded_host: if True (the default), any "X-Forwarded-Host" request header will be used instead of the "Host" header. This is commonly added by HTTP servers (such as Apache) when proxying. - + domains: a dict of {host header value: application} pairs. The incoming "Host" request header is looked up in this dict, and, if a match is found, the corresponding WSGI application @@ -39,17 +39,17 @@ class VirtualHost(object): separate entries for "example.com" and "www.example.com". In addition, "Host" headers may contain the port number. """ - + def __init__(self, default, domains=None, use_x_forwarded_host=True): self.default = default self.domains = domains or {} self.use_x_forwarded_host = use_x_forwarded_host - + def __call__(self, environ, start_response): domain = environ.get('HTTP_HOST', '') if self.use_x_forwarded_host: domain = environ.get("HTTP_X_FORWARDED_HOST", domain) - + nextapp = self.domains.get(domain) if nextapp is None: nextapp = self.default @@ -61,10 +61,10 @@ class VirtualHost(object): class AppResponse(object): - + throws = (KeyboardInterrupt, SystemExit) request = None - + def __init__(self, environ, start_response, cpapp, recursive=False): self.redirections = [] self.recursive = recursive @@ -72,7 +72,7 @@ class AppResponse(object): self.start_response = start_response self.cpapp = cpapp self.setapp() - + def setapp(self): try: self.request = self.get_request() @@ -91,14 +91,14 @@ class AppResponse(object): 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) self.iter_response = iter(b) - + try: self.start_response(s, h, _sys.exc_info()) except: @@ -110,10 +110,10 @@ class AppResponse(object): _cherrypy.log(traceback=True, severity=40) self.close() raise - + 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 @@ -123,7 +123,7 @@ class AppResponse(object): 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. @@ -145,19 +145,19 @@ class AppResponse(object): 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.StringIO() env['CONTENT_LENGTH'] = "0" - + self.setapp() - + def __iter__(self): return self - + def next(self): try: chunk = self.iter_response.next() @@ -180,7 +180,7 @@ class AppResponse(object): 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): @@ -188,7 +188,7 @@ class AppResponse(object): 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: @@ -200,13 +200,13 @@ class AppResponse(object): _cherrypy.log(traceback=True, severity=40) self.close() raise - + return "".join(b) - + 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'] @@ -218,11 +218,11 @@ class AppResponse(object): rfile = self.environ['wsgi.input'] response = self.request.run(meth, path, qs, rproto, headers, rfile) return response.status, response.header_list, response.body - + def get_request(self): """Create a Request object using environ.""" env = self.environ.get - + local = _http.Host('', int(env('SERVER_PORT', 80)), env('SERVER_NAME', '')) remote = _http.Host(env('REMOTE_ADDR', ''), @@ -231,7 +231,7 @@ class AppResponse(object): scheme = env('wsgi.url_scheme') sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) - + # LOGON_USER is served by IIS, and is the name of the # user after having been mapped to a local account. # Both IIS and Apache set REMOTE_USER, when possible. @@ -241,14 +241,14 @@ class AppResponse(object): request.wsgi_environ = self.environ request.prev = env('cherrypy.previous_request', None) return request - + headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', 'CONTENT_LENGTH': 'Content-Length', 'CONTENT_TYPE': 'Content-Type', 'REMOTE_HOST': 'Remote-Host', 'REMOTE_ADDR': 'Remote-Addr', } - + def translate_headers(self, environ): """Translate CGI-environ header names to HTTP header names.""" for cgiName in environ: @@ -263,43 +263,47 @@ class AppResponse(object): class CPWSGIApp(object): """A WSGI application object for a CherryPy Application. - + pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a constructor that takes an initial, positional 'nextapp' argument, plus optional keyword arguments, and returns a WSGI application (that takes environ and start_response arguments). The 'name' can be any you choose, and will correspond to keys in self.config. - + head: rather than nest all apps in the pipeline on each call, it's only done the first time, and the result is memoized into self.head. Set this to None again if you change self.pipeline after calling self. - + config: a dict whose keys match names listed in the pipeline. Each value is a further dict which will be passed to the corresponding named WSGI callable (from the pipeline) as keyword arguments. """ - + pipeline = [] head = None config = {} - + response_class = AppResponse - + def __init__(self, cpapp, pipeline=None): self.cpapp = cpapp self.pipeline = self.pipeline[:] if pipeline: self.pipeline.extend(pipeline) self.config = self.config.copy() - + def tail(self, environ, start_response): """WSGI application callable for the actual CherryPy application. - + You probably shouldn't call this; call self.__call__ instead, so that any WSGI middleware in self.pipeline can run first. """ + # Changed by Kovid as the routes dispatcher cannot handle an empty + # PATH_INFO + if not environ.get('PATH_INFO', True): + environ['PATH_INFO'] = '/' return self.response_class(environ, start_response, self.cpapp) - + def __call__(self, environ, start_response): head = self.head if head is None: @@ -311,7 +315,7 @@ class CPWSGIApp(object): head = callable(head, **conf) self.head = head return head(environ, start_response) - + def namespace_handler(self, k, v): """Config handler for the 'wsgi' namespace.""" if k == "pipeline":