OLD | NEW |
(Empty) | |
| 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): |
| 69 self.default = default |
| 70 self.domains = domains or {} |
| 71 self.use_x_forwarded_host = use_x_forwarded_host |
| 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.thro
ws) |
| 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_respons
e) |
| 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 s
tring." % 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 statu
s |
| 240 # and headers must be bytes masquerading as unicode; that is, th
ey |
| 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 encodin
g, |
| 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 |
OLD | NEW |