Package cherrypy :: Module _cpwsgi
[hide private]
[frames] | no frames]

Source Code for Module cherrypy._cpwsgi

  1  """WSGI interface (see PEP 333 and 3333). 
  2   
  3  Note that WSGI environ keys and values are 'native strings'; that is, 
  4  whatever the type of "" is. For Python 2, that's a byte string; for Python 3, 
  5  it's a unicode string. But PEP 3333 says: "even if Python's str type is 
  6  actually Unicode "under the hood", the content of native strings must 
  7  still be translatable to bytes via the Latin-1 encoding!" 
  8  """ 
  9   
 10  import sys as _sys 
 11   
 12  import cherrypy as _cherrypy 
 13  from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr 
 14  from cherrypy import _cperror 
 15  from cherrypy.lib import httputil 
 16   
 17   
18 -def downgrade_wsgi_ux_to_1x(environ):
19 """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.""" 20 env1x = {} 21 22 url_encoding = environ[ntou('wsgi.url_encoding')] 23 for k, v in list(environ.items()): 24 if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: 25 v = v.encode(url_encoding) 26 elif isinstance(v, unicodestr): 27 v = v.encode('ISO-8859-1') 28 env1x[k.encode('ISO-8859-1')] = v 29 30 return env1x
31 32
33 -class VirtualHost(object):
34 """Select a different WSGI application based on the Host header. 35 36 This can be useful when running multiple sites within one CP server. 37 It allows several domains to point to different applications. For example:: 38 39 root = Root() 40 RootApp = cherrypy.Application(root) 41 Domain2App = cherrypy.Application(root) 42 SecureApp = cherrypy.Application(Secure()) 43 44 vhost = cherrypy._cpwsgi.VirtualHost(RootApp, 45 domains={'www.domain2.example': Domain2App, 46 'www.domain2.example:443': SecureApp, 47 }) 48 49 cherrypy.tree.graft(vhost) 50 """ 51 default = None 52 """Required. The default WSGI application.""" 53 54 use_x_forwarded_host = True 55 """If True (the default), any "X-Forwarded-Host" 56 request header will be used instead of the "Host" header. This 57 is commonly added by HTTP servers (such as Apache) when proxying.""" 58 59 domains = {} 60 """A dict of {host header value: application} pairs. 61 The incoming "Host" request header is looked up in this dict, 62 and, if a match is found, the corresponding WSGI application 63 will be called instead of the default. Note that you often need 64 separate entries for "example.com" and "www.example.com". 65 In addition, "Host" headers may contain the port number. 66 """ 67
68 - def __init__(self, default, domains=None, use_x_forwarded_host=True):
72
73 - def __call__(self, environ, start_response):
74 domain = environ.get('HTTP_HOST', '') 75 if self.use_x_forwarded_host: 76 domain = environ.get("HTTP_X_FORWARDED_HOST", domain) 77 78 nextapp = self.domains.get(domain) 79 if nextapp is None: 80 nextapp = self.default 81 return nextapp(environ, start_response)
82 83
84 -class InternalRedirector(object):
85 """WSGI middleware that handles raised cherrypy.InternalRedirect.""" 86
87 - def __init__(self, nextapp, recursive=False):
88 self.nextapp = nextapp 89 self.recursive = recursive
90
91 - def __call__(self, environ, start_response):
92 redirections = [] 93 while True: 94 environ = environ.copy() 95 try: 96 return self.nextapp(environ, start_response) 97 except _cherrypy.InternalRedirect: 98 ir = _sys.exc_info()[1] 99 sn = environ.get('SCRIPT_NAME', '') 100 path = environ.get('PATH_INFO', '') 101 qs = environ.get('QUERY_STRING', '') 102 103 # Add the *previous* path_info + qs to redirections. 104 old_uri = sn + path 105 if qs: 106 old_uri += "?" + qs 107 redirections.append(old_uri) 108 109 if not self.recursive: 110 # Check to see if the new URI has been redirected to already 111 new_uri = sn + ir.path 112 if ir.query_string: 113 new_uri += "?" + ir.query_string 114 if new_uri in redirections: 115 ir.request.close() 116 raise RuntimeError("InternalRedirector visited the " 117 "same URL twice: %r" % new_uri) 118 119 # Munge the environment and try again. 120 environ['REQUEST_METHOD'] = "GET" 121 environ['PATH_INFO'] = ir.path 122 environ['QUERY_STRING'] = ir.query_string 123 environ['wsgi.input'] = BytesIO() 124 environ['CONTENT_LENGTH'] = "0" 125 environ['cherrypy.previous_request'] = ir.request
126 127
128 -class ExceptionTrapper(object):
129 """WSGI middleware that traps exceptions.""" 130
131 - def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
132 self.nextapp = nextapp 133 self.throws = throws
134
135 - def __call__(self, environ, start_response):
136 return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
137 138
139 -class _TrappedResponse(object):
140 141 response = iter([]) 142
143 - def __init__(self, nextapp, environ, start_response, throws):
144 self.nextapp = nextapp 145 self.environ = environ 146 self.start_response = start_response 147 self.throws = throws 148 self.started_response = False 149 self.response = self.trap(self.nextapp, self.environ, self.start_response) 150 self.iter_response = iter(self.response)
151
152 - def __iter__(self):
153 self.started_response = True 154 return self
155 156 if py3k:
157 - def __next__(self):
158 return self.trap(next, self.iter_response)
159 else:
160 - def next(self):
161 return self.trap(self.iter_response.next)
162
163 - def close(self):
164 if hasattr(self.response, 'close'): 165 self.response.close()
166
167 - def trap(self, func, *args, **kwargs):
168 try: 169 return func(*args, **kwargs) 170 except self.throws: 171 raise 172 except StopIteration: 173 raise 174 except: 175 tb = _cperror.format_exc() 176 #print('trapped (started %s):' % self.started_response, tb) 177 _cherrypy.log(tb, severity=40) 178 if not _cherrypy.request.show_tracebacks: 179 tb = "" 180 s, h, b = _cperror.bare_error(tb) 181 if py3k: 182 # What fun. 183 s = s.decode('ISO-8859-1') 184 h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) 185 for k, v in h] 186 if self.started_response: 187 # Empty our iterable (so future calls raise StopIteration) 188 self.iter_response = iter([]) 189 else: 190 self.iter_response = iter(b) 191 192 try: 193 self.start_response(s, h, _sys.exc_info()) 194 except: 195 # "The application must not trap any exceptions raised by 196 # start_response, if it called start_response with exc_info. 197 # Instead, it should allow such exceptions to propagate 198 # back to the server or gateway." 199 # But we still log and call close() to clean up ourselves. 200 _cherrypy.log(traceback=True, severity=40) 201 raise 202 203 if self.started_response: 204 return ntob("").join(b) 205 else: 206 return b
207 208 209 # WSGI-to-CP Adapter # 210 211
212 -class AppResponse(object):
213 """WSGI response iterable for CherryPy applications.""" 214
215 - def __init__(self, environ, start_response, cpapp):
216 self.cpapp = cpapp 217 try: 218 if not py3k: 219 if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): 220 environ = downgrade_wsgi_ux_to_1x(environ) 221 self.environ = environ 222 self.run() 223 224 r = _cherrypy.serving.response 225 226 outstatus = r.output_status 227 if not isinstance(outstatus, bytestr): 228 raise TypeError("response.output_status is not a byte string.") 229 230 outheaders = [] 231 for k, v in r.header_list: 232 if not isinstance(k, bytestr): 233 raise TypeError("response.header_list key %r is not a byte string." % k) 234 if not isinstance(v, bytestr): 235 raise TypeError("response.header_list value %r is not a byte string." % v) 236 outheaders.append((k, v)) 237 238 if py3k: 239 # According to PEP 3333, when using Python 3, the response status 240 # and headers must be bytes masquerading as unicode; that is, they 241 # must be of type "str" but are restricted to code points in the 242 # "latin-1" set. 243 outstatus = outstatus.decode('ISO-8859-1') 244 outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) 245 for k, v in outheaders] 246 247 self.iter_response = iter(r.body) 248 self.write = start_response(outstatus, outheaders) 249 except: 250 self.close() 251 raise
252
253 - def __iter__(self):
254 return self
255 256 if py3k:
257 - def __next__(self):
258 return next(self.iter_response)
259 else:
260 - def next(self):
261 return self.iter_response.next()
262
263 - def close(self):
264 """Close and de-reference the current request and response. (Core)""" 265 self.cpapp.release_serving()
266
267 - def run(self):
268 """Create a Request object using environ.""" 269 env = self.environ.get 270 271 local = httputil.Host('', int(env('SERVER_PORT', 80)), 272 env('SERVER_NAME', '')) 273 remote = httputil.Host(env('REMOTE_ADDR', ''), 274 int(env('REMOTE_PORT', -1) or -1), 275 env('REMOTE_HOST', '')) 276 scheme = env('wsgi.url_scheme') 277 sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") 278 request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) 279 280 # LOGON_USER is served by IIS, and is the name of the 281 # user after having been mapped to a local account. 282 # Both IIS and Apache set REMOTE_USER, when possible. 283 request.login = env('LOGON_USER') or env('REMOTE_USER') or None 284 request.multithread = self.environ['wsgi.multithread'] 285 request.multiprocess = self.environ['wsgi.multiprocess'] 286 request.wsgi_environ = self.environ 287 request.prev = env('cherrypy.previous_request', None) 288 289 meth = self.environ['REQUEST_METHOD'] 290 291 path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), 292 self.environ.get('PATH_INFO', '')) 293 qs = self.environ.get('QUERY_STRING', '') 294 295 if py3k: 296 # This isn't perfect; if the given PATH_INFO is in the wrong encoding, 297 # it may fail to match the appropriate config section URI. But meh. 298 old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') 299 new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''), 300 "request.uri_encoding", 'utf-8') 301 if new_enc.lower() != old_enc.lower(): 302 # Even though the path and qs are unicode, the WSGI server is 303 # required by PEP 3333 to coerce them to ISO-8859-1 masquerading 304 # as unicode. So we have to encode back to bytes and then decode 305 # again using the "correct" encoding. 306 try: 307 u_path = path.encode(old_enc).decode(new_enc) 308 u_qs = qs.encode(old_enc).decode(new_enc) 309 except (UnicodeEncodeError, UnicodeDecodeError): 310 # Just pass them through without transcoding and hope. 311 pass 312 else: 313 # Only set transcoded values if they both succeed. 314 path = u_path 315 qs = u_qs 316 317 rproto = self.environ.get('SERVER_PROTOCOL') 318 headers = self.translate_headers(self.environ) 319 rfile = self.environ['wsgi.input'] 320 request.run(meth, path, qs, rproto, headers, rfile)
321 322 headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', 323 'CONTENT_LENGTH': 'Content-Length', 324 'CONTENT_TYPE': 'Content-Type', 325 'REMOTE_HOST': 'Remote-Host', 326 'REMOTE_ADDR': 'Remote-Addr', 327 } 328
329 - def translate_headers(self, environ):
330 """Translate CGI-environ header names to HTTP header names.""" 331 for cgiName in environ: 332 # We assume all incoming header keys are uppercase already. 333 if cgiName in self.headerNames: 334 yield self.headerNames[cgiName], environ[cgiName] 335 elif cgiName[:5] == "HTTP_": 336 # Hackish attempt at recovering original header names. 337 translatedHeader = cgiName[5:].replace("_", "-") 338 yield translatedHeader, environ[cgiName]
339 340
341 -class CPWSGIApp(object):
342 """A WSGI application object for a CherryPy Application.""" 343 344 pipeline = [('ExceptionTrapper', ExceptionTrapper), 345 ('InternalRedirector', InternalRedirector), 346 ] 347 """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a 348 constructor that takes an initial, positional 'nextapp' argument, 349 plus optional keyword arguments, and returns a WSGI application 350 (that takes environ and start_response arguments). The 'name' can 351 be any you choose, and will correspond to keys in self.config.""" 352 353 head = None 354 """Rather than nest all apps in the pipeline on each call, it's only 355 done the first time, and the result is memoized into self.head. Set 356 this to None again if you change self.pipeline after calling self.""" 357 358 config = {} 359 """A dict whose keys match names listed in the pipeline. Each 360 value is a further dict which will be passed to the corresponding 361 named WSGI callable (from the pipeline) as keyword arguments.""" 362 363 response_class = AppResponse 364 """The class to instantiate and return as the next app in the WSGI chain.""" 365
366 - def __init__(self, cpapp, pipeline=None):
367 self.cpapp = cpapp 368 self.pipeline = self.pipeline[:] 369 if pipeline: 370 self.pipeline.extend(pipeline) 371 self.config = self.config.copy()
372
373 - def tail(self, environ, start_response):
374 """WSGI application callable for the actual CherryPy application. 375 376 You probably shouldn't call this; call self.__call__ instead, 377 so that any WSGI middleware in self.pipeline can run first. 378 """ 379 return self.response_class(environ, start_response, self.cpapp)
380
381 - def __call__(self, environ, start_response):
382 head = self.head 383 if head is None: 384 # Create and nest the WSGI apps in our pipeline (in reverse order). 385 # Then memoize the result in self.head. 386 head = self.tail 387 for name, callable in self.pipeline[::-1]: 388 conf = self.config.get(name, {}) 389 head = callable(head, **conf) 390 self.head = head 391 return head(environ, start_response)
392
393 - def namespace_handler(self, k, v):
394 """Config handler for the 'wsgi' namespace.""" 395 if k == "pipeline": 396 # Note this allows multiple 'wsgi.pipeline' config entries 397 # (but each entry will be processed in a 'random' order). 398 # It should also allow developers to set default middleware 399 # in code (passed to self.__init__) that deployers can add to 400 # (but not remove) via config. 401 self.pipeline.extend(v) 402 elif k == "response_class": 403 self.response_class = v 404 else: 405 name, arg = k.split(".", 1) 406 bucket = self.config.setdefault(name, {}) 407 bucket[arg] = v
408