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