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