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