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