Content server: When WSGI embedding fix handling of empty URL

This commit is contained in:
Kovid Goyal 2011-09-23 13:28:38 -06:00
parent e03f31d7c0
commit 403eec1609

View File

@ -10,28 +10,28 @@ from cherrypy.lib import http as _http
class VirtualHost(object): class VirtualHost(object):
"""Select a different WSGI application based on the Host header. """Select a different WSGI application based on the Host header.
This can be useful when running multiple sites within one CP server. This can be useful when running multiple sites within one CP server.
It allows several domains to point to different applications. For example: It allows several domains to point to different applications. For example:
root = Root() root = Root()
RootApp = cherrypy.Application(root) RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root) Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure()) SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp, vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App, domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp, 'www.domain2.example:443': SecureApp,
}) })
cherrypy.tree.graft(vhost) cherrypy.tree.graft(vhost)
default: required. The default WSGI application. default: required. The default WSGI application.
use_x_forwarded_host: if True (the default), any "X-Forwarded-Host" use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying. is commonly added by HTTP servers (such as Apache) when proxying.
domains: a dict of {host header value: application} pairs. domains: a dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict, The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding WSGI application 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". separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number. In addition, "Host" headers may contain the port number.
""" """
def __init__(self, default, domains=None, use_x_forwarded_host=True): def __init__(self, default, domains=None, use_x_forwarded_host=True):
self.default = default self.default = default
self.domains = domains or {} self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '') domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host: if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain) domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
nextapp = self.domains.get(domain) nextapp = self.domains.get(domain)
if nextapp is None: if nextapp is None:
nextapp = self.default nextapp = self.default
@ -61,10 +61,10 @@ class VirtualHost(object):
class AppResponse(object): class AppResponse(object):
throws = (KeyboardInterrupt, SystemExit) throws = (KeyboardInterrupt, SystemExit)
request = None request = None
def __init__(self, environ, start_response, cpapp, recursive=False): def __init__(self, environ, start_response, cpapp, recursive=False):
self.redirections = [] self.redirections = []
self.recursive = recursive self.recursive = recursive
@ -72,7 +72,7 @@ class AppResponse(object):
self.start_response = start_response self.start_response = start_response
self.cpapp = cpapp self.cpapp = cpapp
self.setapp() self.setapp()
def setapp(self): def setapp(self):
try: try:
self.request = self.get_request() self.request = self.get_request()
@ -91,14 +91,14 @@ class AppResponse(object):
if getattr(self.request, "throw_errors", False): if getattr(self.request, "throw_errors", False):
self.close() self.close()
raise raise
tb = _cperror.format_exc() tb = _cperror.format_exc()
_cherrypy.log(tb, severity=40) _cherrypy.log(tb, severity=40)
if not getattr(self.request, "show_tracebacks", True): if not getattr(self.request, "show_tracebacks", True):
tb = "" tb = ""
s, h, b = _cperror.bare_error(tb) s, h, b = _cperror.bare_error(tb)
self.iter_response = iter(b) self.iter_response = iter(b)
try: try:
self.start_response(s, h, _sys.exc_info()) self.start_response(s, h, _sys.exc_info())
except: except:
@ -110,10 +110,10 @@ class AppResponse(object):
_cherrypy.log(traceback=True, severity=40) _cherrypy.log(traceback=True, severity=40)
self.close() self.close()
raise raise
def iredirect(self, path, query_string): def iredirect(self, path, query_string):
"""Doctor self.environ and perform an internal redirect. """Doctor self.environ and perform an internal redirect.
When cherrypy.InternalRedirect is raised, this method is called. When cherrypy.InternalRedirect is raised, this method is called.
It rewrites the WSGI environ using the new path and query_string, It rewrites the WSGI environ using the new path and query_string,
and calls a new CherryPy Request object. Because the wsgi.input 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. formed from InternalRedirect.query_string when using that exception.
If you need something more complicated, make and raise your own If you need something more complicated, make and raise your own
exception and write your own AppResponse subclass to trap it. ;) exception and write your own AppResponse subclass to trap it. ;)
It would be a bad idea to redirect after you've already yielded It would be a bad idea to redirect after you've already yielded
response content, although an enterprising soul could choose response content, although an enterprising soul could choose
to abuse this. to abuse this.
@ -145,19 +145,19 @@ class AppResponse(object):
if qs: if qs:
qs = "?" + qs qs = "?" + qs
self.redirections.append(sn + p + qs) self.redirections.append(sn + p + qs)
# Munge environment and try again. # Munge environment and try again.
env['REQUEST_METHOD'] = "GET" env['REQUEST_METHOD'] = "GET"
env['PATH_INFO'] = path env['PATH_INFO'] = path
env['QUERY_STRING'] = query_string env['QUERY_STRING'] = query_string
env['wsgi.input'] = _StringIO.StringIO() env['wsgi.input'] = _StringIO.StringIO()
env['CONTENT_LENGTH'] = "0" env['CONTENT_LENGTH'] = "0"
self.setapp() self.setapp()
def __iter__(self): def __iter__(self):
return self return self
def next(self): def next(self):
try: try:
chunk = self.iter_response.next() chunk = self.iter_response.next()
@ -180,7 +180,7 @@ class AppResponse(object):
if getattr(self.request, "throw_errors", False): if getattr(self.request, "throw_errors", False):
self.close() self.close()
raise raise
tb = _cperror.format_exc() tb = _cperror.format_exc()
_cherrypy.log(tb, severity=40) _cherrypy.log(tb, severity=40)
if not getattr(self.request, "show_tracebacks", True): if not getattr(self.request, "show_tracebacks", True):
@ -188,7 +188,7 @@ class AppResponse(object):
s, h, b = _cperror.bare_error(tb) s, h, b = _cperror.bare_error(tb)
# Empty our iterable (so future calls raise StopIteration) # Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([]) self.iter_response = iter([])
try: try:
self.start_response(s, h, _sys.exc_info()) self.start_response(s, h, _sys.exc_info())
except: except:
@ -200,13 +200,13 @@ class AppResponse(object):
_cherrypy.log(traceback=True, severity=40) _cherrypy.log(traceback=True, severity=40)
self.close() self.close()
raise raise
return "".join(b) return "".join(b)
def close(self): def close(self):
"""Close and de-reference the current request and response. (Core)""" """Close and de-reference the current request and response. (Core)"""
self.cpapp.release_serving() self.cpapp.release_serving()
def get_response(self): def get_response(self):
"""Run self.request and return its response.""" """Run self.request and return its response."""
meth = self.environ['REQUEST_METHOD'] meth = self.environ['REQUEST_METHOD']
@ -218,11 +218,11 @@ class AppResponse(object):
rfile = self.environ['wsgi.input'] rfile = self.environ['wsgi.input']
response = self.request.run(meth, path, qs, rproto, headers, rfile) response = self.request.run(meth, path, qs, rproto, headers, rfile)
return response.status, response.header_list, response.body return response.status, response.header_list, response.body
def get_request(self): def get_request(self):
"""Create a Request object using environ.""" """Create a Request object using environ."""
env = self.environ.get env = self.environ.get
local = _http.Host('', int(env('SERVER_PORT', 80)), local = _http.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', '')) env('SERVER_NAME', ''))
remote = _http.Host(env('REMOTE_ADDR', ''), remote = _http.Host(env('REMOTE_ADDR', ''),
@ -231,7 +231,7 @@ class AppResponse(object):
scheme = env('wsgi.url_scheme') scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the # LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account. # user after having been mapped to a local account.
# Both IIS and Apache set REMOTE_USER, when possible. # Both IIS and Apache set REMOTE_USER, when possible.
@ -241,14 +241,14 @@ class AppResponse(object):
request.wsgi_environ = self.environ request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None) request.prev = env('cherrypy.previous_request', None)
return request return request
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length', 'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type', 'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host', 'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr', 'REMOTE_ADDR': 'Remote-Addr',
} }
def translate_headers(self, environ): def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names.""" """Translate CGI-environ header names to HTTP header names."""
for cgiName in environ: for cgiName in environ:
@ -263,43 +263,47 @@ class AppResponse(object):
class CPWSGIApp(object): class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application. """A WSGI application object for a CherryPy Application.
pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument, constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can (that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config. 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 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 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. this to None again if you change self.pipeline after calling self.
config: a dict whose keys match names listed in the pipeline. Each config: a dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments. named WSGI callable (from the pipeline) as keyword arguments.
""" """
pipeline = [] pipeline = []
head = None head = None
config = {} config = {}
response_class = AppResponse response_class = AppResponse
def __init__(self, cpapp, pipeline=None): def __init__(self, cpapp, pipeline=None):
self.cpapp = cpapp self.cpapp = cpapp
self.pipeline = self.pipeline[:] self.pipeline = self.pipeline[:]
if pipeline: if pipeline:
self.pipeline.extend(pipeline) self.pipeline.extend(pipeline)
self.config = self.config.copy() self.config = self.config.copy()
def tail(self, environ, start_response): def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application. """WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead, You probably shouldn't call this; call self.__call__ instead,
so that any WSGI middleware in self.pipeline can run first. 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) return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
head = self.head head = self.head
if head is None: if head is None:
@ -311,7 +315,7 @@ class CPWSGIApp(object):
head = callable(head, **conf) head = callable(head, **conf)
self.head = head self.head = head
return head(environ, start_response) return head(environ, start_response)
def namespace_handler(self, k, v): def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace.""" """Config handler for the 'wsgi' namespace."""
if k == "pipeline": if k == "pipeline":