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 """ 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 self.cpapp.release_serving()
282
283 - def run(self):
284 """Create a Request object using environ.""" 285 env = self.environ.get 286 287 local = httputil.Host('', int(env('SERVER_PORT', 80)), 288 env('SERVER_NAME', '')) 289 remote = httputil.Host(env('REMOTE_ADDR', ''), 290 int(env('REMOTE_PORT', -1) or -1), 291 env('REMOTE_HOST', '')) 292 scheme = env('wsgi.url_scheme') 293 sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") 294 request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) 295 296 # LOGON_USER is served by IIS, and is the name of the 297 # user after having been mapped to a local account. 298 # Both IIS and Apache set REMOTE_USER, when possible. 299 request.login = env('LOGON_USER') or env('REMOTE_USER') or None 300 request.multithread = self.environ['wsgi.multithread'] 301 request.multiprocess = self.environ['wsgi.multiprocess'] 302 request.wsgi_environ = self.environ 303 request.prev = env('cherrypy.previous_request', None) 304 305 meth = self.environ['REQUEST_METHOD'] 306 307 path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), 308 self.environ.get('PATH_INFO', '')) 309 qs = self.environ.get('QUERY_STRING', '') 310 311 if py3k: 312 # This isn't perfect; if the given PATH_INFO is in the 313 # wrong encoding, it may fail to match the appropriate config 314 # section URI. But meh. 315 old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') 316 new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''), 317 "request.uri_encoding", 'utf-8') 318 if new_enc.lower() != old_enc.lower(): 319 # Even though the path and qs are unicode, the WSGI server 320 # is required by PEP 3333 to coerce them to ISO-8859-1 321 # masquerading as unicode. So we have to encode back to 322 # bytes and then decode again using the "correct" encoding. 323 try: 324 u_path = path.encode(old_enc).decode(new_enc) 325 u_qs = qs.encode(old_enc).decode(new_enc) 326 except (UnicodeEncodeError, UnicodeDecodeError): 327 # Just pass them through without transcoding and hope. 328 pass 329 else: 330 # Only set transcoded values if they both succeed. 331 path = u_path 332 qs = u_qs 333 334 rproto = self.environ.get('SERVER_PROTOCOL') 335 headers = self.translate_headers(self.environ) 336 rfile = self.environ['wsgi.input'] 337 request.run(meth, path, qs, rproto, headers, rfile)
338 339 headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', 340 'CONTENT_LENGTH': 'Content-Length', 341 'CONTENT_TYPE': 'Content-Type', 342 'REMOTE_HOST': 'Remote-Host', 343 'REMOTE_ADDR': 'Remote-Addr', 344 } 345
346 - def translate_headers(self, environ):
347 """Translate CGI-environ header names to HTTP header names.""" 348 for cgiName in environ: 349 # We assume all incoming header keys are uppercase already. 350 if cgiName in self.headerNames: 351 yield self.headerNames[cgiName], environ[cgiName] 352 elif cgiName[:5] == "HTTP_": 353 # Hackish attempt at recovering original header names. 354 translatedHeader = cgiName[5:].replace("_", "-") 355 yield translatedHeader, environ[cgiName]
356 357
358 -class CPWSGIApp(object):
359 360 """A WSGI application object for a CherryPy Application.""" 361 362 pipeline = [('ExceptionTrapper', ExceptionTrapper), 363 ('InternalRedirector', InternalRedirector), 364 ] 365 """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a 366 constructor that takes an initial, positional 'nextapp' argument, 367 plus optional keyword arguments, and returns a WSGI application 368 (that takes environ and start_response arguments). The 'name' can 369 be any you choose, and will correspond to keys in self.config.""" 370 371 head = None 372 """Rather than nest all apps in the pipeline on each call, it's only 373 done the first time, and the result is memoized into self.head. Set 374 this to None again if you change self.pipeline after calling self.""" 375 376 config = {} 377 """A dict whose keys match names listed in the pipeline. Each 378 value is a further dict which will be passed to the corresponding 379 named WSGI callable (from the pipeline) as keyword arguments.""" 380 381 response_class = AppResponse 382 """The class to instantiate and return as the next app in the WSGI chain. 383 """ 384
385 - def __init__(self, cpapp, pipeline=None):
386 self.cpapp = cpapp 387 self.pipeline = self.pipeline[:] 388 if pipeline: 389 self.pipeline.extend(pipeline) 390 self.config = self.config.copy()
391
392 - def tail(self, environ, start_response):
393 """WSGI application callable for the actual CherryPy application. 394 395 You probably shouldn't call this; call self.__call__ instead, 396 so that any WSGI middleware in self.pipeline can run first. 397 """ 398 return self.response_class(environ, start_response, self.cpapp)
399
400 - def __call__(self, environ, start_response):
401 head = self.head 402 if head is None: 403 # Create and nest the WSGI apps in our pipeline (in reverse order). 404 # Then memoize the result in self.head. 405 head = self.tail 406 for name, callable in self.pipeline[::-1]: 407 conf = self.config.get(name, {}) 408 head = callable(head, **conf) 409 self.head = head 410 return head(environ, start_response)
411
412 - def namespace_handler(self, k, v):
413 """Config handler for the 'wsgi' namespace.""" 414 if k == "pipeline": 415 # Note this allows multiple 'wsgi.pipeline' config entries 416 # (but each entry will be processed in a 'random' order). 417 # It should also allow developers to set default middleware 418 # in code (passed to self.__init__) that deployers can add to 419 # (but not remove) via config. 420 self.pipeline.extend(v) 421 elif k == "response_class": 422 self.response_class = v 423 else: 424 name, arg = k.split(".", 1) 425 bucket = self.config.setdefault(name, {}) 426 bucket[arg] = v
427