1# 2# Copyright 2009 Facebook 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16"""WSGI support for the Tornado web framework. 17 18WSGI is the Python standard for web servers, and allows for interoperability 19between Tornado and other Python web frameworks and servers. This module 20provides WSGI support in two ways: 21 22* `WSGIAdapter` converts a `tornado.web.Application` to the WSGI application 23 interface. This is useful for running a Tornado app on another 24 HTTP server, such as Google App Engine. See the `WSGIAdapter` class 25 documentation for limitations that apply. 26* `WSGIContainer` lets you run other WSGI applications and frameworks on the 27 Tornado HTTP server. For example, with this class you can mix Django 28 and Tornado handlers in a single server. 29""" 30 31from __future__ import absolute_import, division, print_function 32 33import sys 34from io import BytesIO 35import tornado 36import warnings 37 38from tornado.concurrent import Future 39from tornado import escape 40from tornado import httputil 41from tornado.log import access_log 42from tornado import web 43from tornado.escape import native_str 44from tornado.util import unicode_type, PY3 45 46 47if PY3: 48 import urllib.parse as urllib_parse # py3 49else: 50 import urllib as urllib_parse 51 52# PEP 3333 specifies that WSGI on python 3 generally deals with byte strings 53# that are smuggled inside objects of type unicode (via the latin1 encoding). 54# These functions are like those in the tornado.escape module, but defined 55# here to minimize the temptation to use them in non-wsgi contexts. 56if str is unicode_type: 57 def to_wsgi_str(s): 58 assert isinstance(s, bytes) 59 return s.decode('latin1') 60 61 def from_wsgi_str(s): 62 assert isinstance(s, str) 63 return s.encode('latin1') 64else: 65 def to_wsgi_str(s): 66 assert isinstance(s, bytes) 67 return s 68 69 def from_wsgi_str(s): 70 assert isinstance(s, str) 71 return s 72 73 74class WSGIApplication(web.Application): 75 """A WSGI equivalent of `tornado.web.Application`. 76 77 .. deprecated:: 4.0 78 79 Use a regular `.Application` and wrap it in `WSGIAdapter` instead. 80 This class will be removed in Tornado 6.0. 81 """ 82 def __call__(self, environ, start_response): 83 return WSGIAdapter(self)(environ, start_response) 84 85 86# WSGI has no facilities for flow control, so just return an already-done 87# Future when the interface requires it. 88def _dummy_future(): 89 f = Future() 90 f.set_result(None) 91 return f 92 93 94class _WSGIConnection(httputil.HTTPConnection): 95 def __init__(self, method, start_response, context): 96 self.method = method 97 self.start_response = start_response 98 self.context = context 99 self._write_buffer = [] 100 self._finished = False 101 self._expected_content_remaining = None 102 self._error = None 103 104 def set_close_callback(self, callback): 105 # WSGI has no facility for detecting a closed connection mid-request, 106 # so we can simply ignore the callback. 107 pass 108 109 def write_headers(self, start_line, headers, chunk=None, callback=None): 110 if self.method == 'HEAD': 111 self._expected_content_remaining = 0 112 elif 'Content-Length' in headers: 113 self._expected_content_remaining = int(headers['Content-Length']) 114 else: 115 self._expected_content_remaining = None 116 self.start_response( 117 '%s %s' % (start_line.code, start_line.reason), 118 [(native_str(k), native_str(v)) for (k, v) in headers.get_all()]) 119 if chunk is not None: 120 self.write(chunk, callback) 121 elif callback is not None: 122 callback() 123 return _dummy_future() 124 125 def write(self, chunk, callback=None): 126 if self._expected_content_remaining is not None: 127 self._expected_content_remaining -= len(chunk) 128 if self._expected_content_remaining < 0: 129 self._error = httputil.HTTPOutputError( 130 "Tried to write more data than Content-Length") 131 raise self._error 132 self._write_buffer.append(chunk) 133 if callback is not None: 134 callback() 135 return _dummy_future() 136 137 def finish(self): 138 if (self._expected_content_remaining is not None and 139 self._expected_content_remaining != 0): 140 self._error = httputil.HTTPOutputError( 141 "Tried to write %d bytes less than Content-Length" % 142 self._expected_content_remaining) 143 raise self._error 144 self._finished = True 145 146 147class _WSGIRequestContext(object): 148 def __init__(self, remote_ip, protocol): 149 self.remote_ip = remote_ip 150 self.protocol = protocol 151 152 def __str__(self): 153 return self.remote_ip 154 155 156class WSGIAdapter(object): 157 """Converts a `tornado.web.Application` instance into a WSGI application. 158 159 Example usage:: 160 161 import tornado.web 162 import tornado.wsgi 163 import wsgiref.simple_server 164 165 class MainHandler(tornado.web.RequestHandler): 166 def get(self): 167 self.write("Hello, world") 168 169 if __name__ == "__main__": 170 application = tornado.web.Application([ 171 (r"/", MainHandler), 172 ]) 173 wsgi_app = tornado.wsgi.WSGIAdapter(application) 174 server = wsgiref.simple_server.make_server('', 8888, wsgi_app) 175 server.serve_forever() 176 177 See the `appengine demo 178 <https://github.com/tornadoweb/tornado/tree/stable/demos/appengine>`_ 179 for an example of using this module to run a Tornado app on Google 180 App Engine. 181 182 In WSGI mode asynchronous methods are not supported. This means 183 that it is not possible to use `.AsyncHTTPClient`, or the 184 `tornado.auth` or `tornado.websocket` modules. 185 186 In multithreaded WSGI servers on Python 3, it may be necessary to 187 permit `asyncio` to create event loops on any thread. Run the 188 following at startup (typically import time for WSGI 189 applications):: 190 191 import asyncio 192 from tornado.platform.asyncio import AnyThreadEventLoopPolicy 193 asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy()) 194 195 .. versionadded:: 4.0 196 197 .. deprecated:: 5.1 198 199 This class is deprecated and will be removed in Tornado 6.0. 200 Use Tornado's `.HTTPServer` instead of a WSGI container. 201 """ 202 def __init__(self, application): 203 warnings.warn("WSGIAdapter is deprecated, use Tornado's HTTPServer instead", 204 DeprecationWarning) 205 if isinstance(application, WSGIApplication): 206 self.application = lambda request: web.Application.__call__( 207 application, request) 208 else: 209 self.application = application 210 211 def __call__(self, environ, start_response): 212 method = environ["REQUEST_METHOD"] 213 uri = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) 214 uri += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) 215 if environ.get("QUERY_STRING"): 216 uri += "?" + environ["QUERY_STRING"] 217 headers = httputil.HTTPHeaders() 218 if environ.get("CONTENT_TYPE"): 219 headers["Content-Type"] = environ["CONTENT_TYPE"] 220 if environ.get("CONTENT_LENGTH"): 221 headers["Content-Length"] = environ["CONTENT_LENGTH"] 222 for key in environ: 223 if key.startswith("HTTP_"): 224 headers[key[5:].replace("_", "-")] = environ[key] 225 if headers.get("Content-Length"): 226 body = environ["wsgi.input"].read( 227 int(headers["Content-Length"])) 228 else: 229 body = b"" 230 protocol = environ["wsgi.url_scheme"] 231 remote_ip = environ.get("REMOTE_ADDR", "") 232 if environ.get("HTTP_HOST"): 233 host = environ["HTTP_HOST"] 234 else: 235 host = environ["SERVER_NAME"] 236 connection = _WSGIConnection(method, start_response, 237 _WSGIRequestContext(remote_ip, protocol)) 238 request = httputil.HTTPServerRequest( 239 method, uri, "HTTP/1.1", headers=headers, body=body, 240 host=host, connection=connection) 241 request._parse_body() 242 self.application(request) 243 if connection._error: 244 raise connection._error 245 if not connection._finished: 246 raise Exception("request did not finish synchronously") 247 return connection._write_buffer 248 249 250class WSGIContainer(object): 251 r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server. 252 253 .. warning:: 254 255 WSGI is a *synchronous* interface, while Tornado's concurrency model 256 is based on single-threaded asynchronous execution. This means that 257 running a WSGI app with Tornado's `WSGIContainer` is *less scalable* 258 than running the same app in a multi-threaded WSGI server like 259 ``gunicorn`` or ``uwsgi``. Use `WSGIContainer` only when there are 260 benefits to combining Tornado and WSGI in the same process that 261 outweigh the reduced scalability. 262 263 Wrap a WSGI function in a `WSGIContainer` and pass it to `.HTTPServer` to 264 run it. For example:: 265 266 def simple_app(environ, start_response): 267 status = "200 OK" 268 response_headers = [("Content-type", "text/plain")] 269 start_response(status, response_headers) 270 return ["Hello world!\n"] 271 272 container = tornado.wsgi.WSGIContainer(simple_app) 273 http_server = tornado.httpserver.HTTPServer(container) 274 http_server.listen(8888) 275 tornado.ioloop.IOLoop.current().start() 276 277 This class is intended to let other frameworks (Django, web.py, etc) 278 run on the Tornado HTTP server and I/O loop. 279 280 The `tornado.web.FallbackHandler` class is often useful for mixing 281 Tornado and WSGI apps in the same server. See 282 https://github.com/bdarnell/django-tornado-demo for a complete example. 283 """ 284 def __init__(self, wsgi_application): 285 self.wsgi_application = wsgi_application 286 287 def __call__(self, request): 288 data = {} 289 response = [] 290 291 def start_response(status, response_headers, exc_info=None): 292 data["status"] = status 293 data["headers"] = response_headers 294 return response.append 295 app_response = self.wsgi_application( 296 WSGIContainer.environ(request), start_response) 297 try: 298 response.extend(app_response) 299 body = b"".join(response) 300 finally: 301 if hasattr(app_response, "close"): 302 app_response.close() 303 if not data: 304 raise Exception("WSGI app did not call start_response") 305 306 status_code, reason = data["status"].split(' ', 1) 307 status_code = int(status_code) 308 headers = data["headers"] 309 header_set = set(k.lower() for (k, v) in headers) 310 body = escape.utf8(body) 311 if status_code != 304: 312 if "content-length" not in header_set: 313 headers.append(("Content-Length", str(len(body)))) 314 if "content-type" not in header_set: 315 headers.append(("Content-Type", "text/html; charset=UTF-8")) 316 if "server" not in header_set: 317 headers.append(("Server", "TornadoServer/%s" % tornado.version)) 318 319 start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason) 320 header_obj = httputil.HTTPHeaders() 321 for key, value in headers: 322 header_obj.add(key, value) 323 request.connection.write_headers(start_line, header_obj, chunk=body) 324 request.connection.finish() 325 self._log(status_code, request) 326 327 @staticmethod 328 def environ(request): 329 """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment. 330 """ 331 hostport = request.host.split(":") 332 if len(hostport) == 2: 333 host = hostport[0] 334 port = int(hostport[1]) 335 else: 336 host = request.host 337 port = 443 if request.protocol == "https" else 80 338 environ = { 339 "REQUEST_METHOD": request.method, 340 "SCRIPT_NAME": "", 341 "PATH_INFO": to_wsgi_str(escape.url_unescape( 342 request.path, encoding=None, plus=False)), 343 "QUERY_STRING": request.query, 344 "REMOTE_ADDR": request.remote_ip, 345 "SERVER_NAME": host, 346 "SERVER_PORT": str(port), 347 "SERVER_PROTOCOL": request.version, 348 "wsgi.version": (1, 0), 349 "wsgi.url_scheme": request.protocol, 350 "wsgi.input": BytesIO(escape.utf8(request.body)), 351 "wsgi.errors": sys.stderr, 352 "wsgi.multithread": False, 353 "wsgi.multiprocess": True, 354 "wsgi.run_once": False, 355 } 356 if "Content-Type" in request.headers: 357 environ["CONTENT_TYPE"] = request.headers.pop("Content-Type") 358 if "Content-Length" in request.headers: 359 environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length") 360 for key, value in request.headers.items(): 361 environ["HTTP_" + key.replace("-", "_").upper()] = value 362 return environ 363 364 def _log(self, status_code, request): 365 if status_code < 400: 366 log_method = access_log.info 367 elif status_code < 500: 368 log_method = access_log.warning 369 else: 370 log_method = access_log.error 371 request_time = 1000.0 * request.request_time() 372 summary = request.method + " " + request.uri + " (" + \ 373 request.remote_ip + ")" 374 log_method("%d %s %.2fms", status_code, summary, request_time) 375 376 377HTTPRequest = httputil.HTTPServerRequest 378