codekingpro/portable-devtools
114k
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.
20
21This module provides WSGI support via the `WSGIContainer` class, which
22makes it possible to run applications using other WSGI frameworks on
23the Tornado HTTP server. The reverse is not supported; the Tornado
24`.Application` and `.RequestHandler` classes are designed for use with
25the Tornado `.HTTPServer` and cannot be used in a generic WSGI
26container.
27
28"""
29
30import concurrent.futures
31from io import BytesIO
32import tornado
33import sys
34
35from tornado.concurrent import dummy_executor
36from tornado import escape
37from tornado import httputil
38from tornado.ioloop import IOLoop
39from tornado.log import access_log
40
41from typing import List, Tuple, Optional, Callable, Any, Dict
42from types import TracebackType
43import typing
44
45if typing.TYPE_CHECKING:
46 from typing import Type # noqa: F401
47 from _typeshed.wsgi import WSGIApplication as WSGIAppType # noqa: F401
48
49
50# PEP 3333 specifies that WSGI on python 3 generally deals with byte strings
51# that are smuggled inside objects of type unicode (via the latin1 encoding).
52# This function is like those in the tornado.escape module, but defined
53# here to minimize the temptation to use it in non-wsgi contexts.
54def to_wsgi_str(s: bytes) -> str:
55 assert isinstance(s, bytes)
56 return s.decode("latin1")
57
58
59class WSGIContainer:
60 r"""Makes a WSGI-compatible application runnable on Tornado's HTTP server.
61
62 .. warning::
63
64 WSGI is a *synchronous* interface, while Tornado's concurrency model
65 is based on single-threaded *asynchronous* execution. Many of Tornado's
66 distinguishing features are not available in WSGI mode, including efficient
67 long-polling and websockets. The primary purpose of `WSGIContainer` is
68 to support both WSGI applications and native Tornado ``RequestHandlers`` in
69 a single process. WSGI-only applications are likely to be better off
70 with a dedicated WSGI server such as ``gunicorn`` or ``uwsgi``.
71
72 Wrap a WSGI application in a `WSGIContainer` to make it implement the Tornado
73 `.HTTPServer` ``request_callback`` interface. The `WSGIContainer` object can
74 then be passed to classes from the `tornado.routing` module,
75 `tornado.web.FallbackHandler`, or to `.HTTPServer` directly.
76
77 This class is intended to let other frameworks (Django, Flask, etc)
78 run on the Tornado HTTP server and I/O loop.
79
80 Realistic usage will be more complicated, but the simplest possible example uses a
81 hand-written WSGI application with `.HTTPServer`::
82
83 def simple_app(environ, start_response):
84 status = "200 OK"
85 response_headers = [("Content-type", "text/plain")]
86 start_response(status, response_headers)
87 return [b"Hello world!\n"]
88
89 async def main():
90 container = tornado.wsgi.WSGIContainer(simple_app)
91 http_server = tornado.httpserver.HTTPServer(container)
92 http_server.listen(8888)
93 await asyncio.Event().wait()
94
95 asyncio.run(main())
96
97 The recommended pattern is to use the `tornado.routing` module to set up routing
98 rules between your WSGI application and, typically, a `tornado.web.Application`.
99 Alternatively, `tornado.web.Application` can be used as the top-level router
100 and `tornado.web.FallbackHandler` can embed a `WSGIContainer` within it.
101
102 If the ``executor`` argument is provided, the WSGI application will be executed
103 on that executor. This must be an instance of `concurrent.futures.Executor`,
104 typically a ``ThreadPoolExecutor`` (``ProcessPoolExecutor`` is not supported).
105 If no ``executor`` is given, the application will run on the event loop thread in
106 Tornado 6.3; this will change to use an internal thread pool by default in
107 Tornado 7.0.
108
109 .. warning::
110 By default, the WSGI application is executed on the event loop's thread. This
111 limits the server to one request at a time (per process), making it less scalable
112 than most other WSGI servers. It is therefore highly recommended that you pass
113 a ``ThreadPoolExecutor`` when constructing the `WSGIContainer`, after verifying
114 that your application is thread-safe. The default will change to use a
115 ``ThreadPoolExecutor`` in Tornado 7.0.
116
117 .. versionadded:: 6.3
118 The ``executor`` parameter.
119
120 .. deprecated:: 6.3
121 The default behavior of running the WSGI application on the event loop thread
122 is deprecated and will change in Tornado 7.0 to use a thread pool by default.
123 """
124
125 def __init__(
126 self,
127 wsgi_application: "WSGIAppType",
128 executor: Optional[concurrent.futures.Executor] = None,
129 ) -> None:
130 self.wsgi_application = wsgi_application
131 self.executor = dummy_executor if executor is None else executor
132
133 def __call__(self, request: httputil.HTTPServerRequest) -> None:
134 IOLoop.current().spawn_callback(self.handle_request, request)
135
136 async def handle_request(self, request: httputil.HTTPServerRequest) -> None:
137 data = {} # type: Dict[str, Any]
138 response = [] # type: List[bytes]
139
140 def start_response(
141 status: str,
142 headers: List[Tuple[str, str]],
143 exc_info: Optional[
144 Tuple[
145 "Optional[Type[BaseException]]",
146 Optional[BaseException],
147 Optional[TracebackType],
148 ]
149 ] = None,
150 ) -> Callable[[bytes], Any]:
151 data["status"] = status
152 data["headers"] = headers
153 return response.append
154
155 loop = IOLoop.current()
156 app_response = await loop.run_in_executor(
157 self.executor,
158 self.wsgi_application,
159 self.environ(request),
160 start_response,
161 )
162 try:
163 app_response_iter = iter(app_response)
164
165 def next_chunk() -> Optional[bytes]:
166 try:
167 return next(app_response_iter)
168 except StopIteration:
169 # StopIteration is special and is not allowed to pass through
170 # coroutines normally.
171 return None
172
173 while True:
174 chunk = await loop.run_in_executor(self.executor, next_chunk)
175 if chunk is None:
176 break
177 response.append(chunk)
178 finally:
179 if hasattr(app_response, "close"):
180 app_response.close() # type: ignore
181 body = b"".join(response)
182 if not data:
183 raise Exception("WSGI app did not call start_response")
184
185 status_code_str, reason = data["status"].split(" ", 1)
186 status_code = int(status_code_str)
187 headers = data["headers"] # type: List[Tuple[str, str]]
188 header_set = {k.lower() for (k, v) in headers}
189 body = escape.utf8(body)
190 if status_code != 304:
191 if "content-length" not in header_set:
192 headers.append(("Content-Length", str(len(body))))
193 if "content-type" not in header_set:
194 headers.append(("Content-Type", "text/html; charset=UTF-8"))
195 if "server" not in header_set:
196 headers.append(("Server", "TornadoServer/%s" % tornado.version))
197
198 start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
199 header_obj = httputil.HTTPHeaders()
200 for key, value in headers:
201 header_obj.add(key, value)
202 assert request.connection is not None
203 request.connection.write_headers(start_line, header_obj, chunk=body)
204 request.connection.finish()
205 self._log(status_code, request)
206
207 def environ(self, request: httputil.HTTPServerRequest) -> Dict[str, Any]:
208 """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
209
210 .. versionchanged:: 6.3
211 No longer a static method.
212 """
213 hostport = request.host.split(":")
214 if len(hostport) == 2:
215 host = hostport[0]
216 port = int(hostport[1])
217 else:
218 host = request.host
219 port = 443 if request.protocol == "https" else 80
220 environ = {
221 "REQUEST_METHOD": request.method,
222 "SCRIPT_NAME": "",
223 "PATH_INFO": to_wsgi_str(
224 escape.url_unescape(request.path, encoding=None, plus=False)
225 ),
226 "QUERY_STRING": request.query,
227 "REMOTE_ADDR": request.remote_ip,
228 "SERVER_NAME": host,
229 "SERVER_PORT": str(port),
230 "SERVER_PROTOCOL": request.version,
231 "wsgi.version": (1, 0),
232 "wsgi.url_scheme": request.protocol,
233 "wsgi.input": BytesIO(escape.utf8(request.body)),
234 "wsgi.errors": sys.stderr,
235 "wsgi.multithread": self.executor is not dummy_executor,
236 "wsgi.multiprocess": True,
237 "wsgi.run_once": False,
238 }
239 if "Content-Type" in request.headers:
240 environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
241 if "Content-Length" in request.headers:
242 environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
243 for key, value in request.headers.items():
244 environ["HTTP_" + key.replace("-", "_").upper()] = value
245 return environ
246
247 def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
248 if status_code < 400:
249 log_method = access_log.info
250 elif status_code < 500:
251 log_method = access_log.warning
252 else:
253 log_method = access_log.error
254 request_time = 1000.0 * request.request_time()
255 assert request.method is not None
256 assert request.uri is not None
257 summary = (
258 request.method # type: ignore[operator]
259 + " "
260 + request.uri
261 + " ("
262 + request.remote_ip
263 + ")"
264 )
265 log_method("%d %s %.2fms", status_code, summary, request_time)
266
267
268HTTPRequest = httputil.HTTPServerRequest
269 