00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017 """WSGI support for the Tornado web framework.
00018
00019 WSGI is the Python standard for web servers, and allows for interoperability
00020 between Tornado and other Python web frameworks and servers. This module
00021 provides WSGI support in two ways:
00022
00023 * `WSGIAdapter` converts a `tornado.web.Application` to the WSGI application
00024 interface. This is useful for running a Tornado app on another
00025 HTTP server, such as Google App Engine. See the `WSGIAdapter` class
00026 documentation for limitations that apply.
00027 * `WSGIContainer` lets you run other WSGI applications and frameworks on the
00028 Tornado HTTP server. For example, with this class you can mix Django
00029 and Tornado handlers in a single server.
00030 """
00031
00032 from __future__ import absolute_import, division, print_function, with_statement
00033
00034 import sys
00035 import tornado
00036
00037 from tornado.concurrent import Future
00038 from tornado import escape
00039 from tornado import httputil
00040 from tornado.log import access_log
00041 from tornado import web
00042 from tornado.escape import native_str
00043 from tornado.util import bytes_type, unicode_type
00044
00045 try:
00046 from io import BytesIO
00047 except ImportError:
00048 from cStringIO import StringIO as BytesIO
00049
00050 try:
00051 import urllib.parse as urllib_parse
00052 except ImportError:
00053 import urllib as urllib_parse
00054
00055
00056
00057
00058
00059 if str is unicode_type:
00060 def to_wsgi_str(s):
00061 assert isinstance(s, bytes_type)
00062 return s.decode('latin1')
00063
00064 def from_wsgi_str(s):
00065 assert isinstance(s, str)
00066 return s.encode('latin1')
00067 else:
00068 def to_wsgi_str(s):
00069 assert isinstance(s, bytes_type)
00070 return s
00071
00072 def from_wsgi_str(s):
00073 assert isinstance(s, str)
00074 return s
00075
00076
00077 class WSGIApplication(web.Application):
00078 """A WSGI equivalent of `tornado.web.Application`.
00079
00080 .. deprecated:: 4.0
00081
00082 Use a regular `.Application` and wrap it in `WSGIAdapter` instead.
00083 """
00084 def __call__(self, environ, start_response):
00085 return WSGIAdapter(self)(environ, start_response)
00086
00087
00088
00089
00090 _dummy_future = Future()
00091 _dummy_future.set_result(None)
00092
00093
00094 class _WSGIConnection(httputil.HTTPConnection):
00095 def __init__(self, method, start_response, context):
00096 self.method = method
00097 self.start_response = start_response
00098 self.context = context
00099 self._write_buffer = []
00100 self._finished = False
00101 self._expected_content_remaining = None
00102 self._error = None
00103
00104 def set_close_callback(self, callback):
00105
00106
00107 pass
00108
00109 def write_headers(self, start_line, headers, chunk=None, callback=None):
00110 if self.method == 'HEAD':
00111 self._expected_content_remaining = 0
00112 elif 'Content-Length' in headers:
00113 self._expected_content_remaining = int(headers['Content-Length'])
00114 else:
00115 self._expected_content_remaining = None
00116 self.start_response(
00117 '%s %s' % (start_line.code, start_line.reason),
00118 [(native_str(k), native_str(v)) for (k, v) in headers.get_all()])
00119 if chunk is not None:
00120 self.write(chunk, callback)
00121 elif callback is not None:
00122 callback()
00123 return _dummy_future
00124
00125 def write(self, chunk, callback=None):
00126 if self._expected_content_remaining is not None:
00127 self._expected_content_remaining -= len(chunk)
00128 if self._expected_content_remaining < 0:
00129 self._error = httputil.HTTPOutputError(
00130 "Tried to write more data than Content-Length")
00131 raise self._error
00132 self._write_buffer.append(chunk)
00133 if callback is not None:
00134 callback()
00135 return _dummy_future
00136
00137 def finish(self):
00138 if (self._expected_content_remaining is not None and
00139 self._expected_content_remaining != 0):
00140 self._error = httputil.HTTPOutputError(
00141 "Tried to write %d bytes less than Content-Length" %
00142 self._expected_content_remaining)
00143 raise self._error
00144 self._finished = True
00145
00146
00147 class _WSGIRequestContext(object):
00148 def __init__(self, remote_ip, protocol):
00149 self.remote_ip = remote_ip
00150 self.protocol = protocol
00151
00152 def __str__(self):
00153 return self.remote_ip
00154
00155
00156 class WSGIAdapter(object):
00157 """Converts a `tornado.web.Application` instance into a WSGI application.
00158
00159 Example usage::
00160
00161 import tornado.web
00162 import tornado.wsgi
00163 import wsgiref.simple_server
00164
00165 class MainHandler(tornado.web.RequestHandler):
00166 def get(self):
00167 self.write("Hello, world")
00168
00169 if __name__ == "__main__":
00170 application = tornado.web.Application([
00171 (r"/", MainHandler),
00172 ])
00173 wsgi_app = tornado.wsgi.WSGIAdapter(application)
00174 server = wsgiref.simple_server.make_server('', 8888, wsgi_app)
00175 server.serve_forever()
00176
00177 See the `appengine demo
00178 <https://github.com/tornadoweb/tornado/tree/stable/demos/appengine>`_
00179 for an example of using this module to run a Tornado app on Google
00180 App Engine.
00181
00182 In WSGI mode asynchronous methods are not supported. This means
00183 that it is not possible to use `.AsyncHTTPClient`, or the
00184 `tornado.auth` or `tornado.websocket` modules.
00185
00186 .. versionadded:: 4.0
00187 """
00188 def __init__(self, application):
00189 if isinstance(application, WSGIApplication):
00190 self.application = lambda request: web.Application.__call__(
00191 application, request)
00192 else:
00193 self.application = application
00194
00195 def __call__(self, environ, start_response):
00196 method = environ["REQUEST_METHOD"]
00197 uri = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", "")))
00198 uri += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", "")))
00199 if environ.get("QUERY_STRING"):
00200 uri += "?" + environ["QUERY_STRING"]
00201 headers = httputil.HTTPHeaders()
00202 if environ.get("CONTENT_TYPE"):
00203 headers["Content-Type"] = environ["CONTENT_TYPE"]
00204 if environ.get("CONTENT_LENGTH"):
00205 headers["Content-Length"] = environ["CONTENT_LENGTH"]
00206 for key in environ:
00207 if key.startswith("HTTP_"):
00208 headers[key[5:].replace("_", "-")] = environ[key]
00209 if headers.get("Content-Length"):
00210 body = environ["wsgi.input"].read(
00211 int(headers["Content-Length"]))
00212 else:
00213 body = ""
00214 protocol = environ["wsgi.url_scheme"]
00215 remote_ip = environ.get("REMOTE_ADDR", "")
00216 if environ.get("HTTP_HOST"):
00217 host = environ["HTTP_HOST"]
00218 else:
00219 host = environ["SERVER_NAME"]
00220 connection = _WSGIConnection(method, start_response,
00221 _WSGIRequestContext(remote_ip, protocol))
00222 request = httputil.HTTPServerRequest(
00223 method, uri, "HTTP/1.1", headers=headers, body=body,
00224 host=host, connection=connection)
00225 request._parse_body()
00226 self.application(request)
00227 if connection._error:
00228 raise connection._error
00229 if not connection._finished:
00230 raise Exception("request did not finish synchronously")
00231 return connection._write_buffer
00232
00233
00234 class WSGIContainer(object):
00235 r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
00236
00237 .. warning::
00238
00239 WSGI is a *synchronous* interface, while Tornado's concurrency model
00240 is based on single-threaded asynchronous execution. This means that
00241 running a WSGI app with Tornado's `WSGIContainer` is *less scalable*
00242 than running the same app in a multi-threaded WSGI server like
00243 ``gunicorn`` or ``uwsgi``. Use `WSGIContainer` only when there are
00244 benefits to combining Tornado and WSGI in the same process that
00245 outweigh the reduced scalability.
00246
00247 Wrap a WSGI function in a `WSGIContainer` and pass it to `.HTTPServer` to
00248 run it. For example::
00249
00250 def simple_app(environ, start_response):
00251 status = "200 OK"
00252 response_headers = [("Content-type", "text/plain")]
00253 start_response(status, response_headers)
00254 return ["Hello world!\n"]
00255
00256 container = tornado.wsgi.WSGIContainer(simple_app)
00257 http_server = tornado.httpserver.HTTPServer(container)
00258 http_server.listen(8888)
00259 tornado.ioloop.IOLoop.instance().start()
00260
00261 This class is intended to let other frameworks (Django, web.py, etc)
00262 run on the Tornado HTTP server and I/O loop.
00263
00264 The `tornado.web.FallbackHandler` class is often useful for mixing
00265 Tornado and WSGI apps in the same server. See
00266 https://github.com/bdarnell/django-tornado-demo for a complete example.
00267 """
00268 def __init__(self, wsgi_application):
00269 self.wsgi_application = wsgi_application
00270
00271 def __call__(self, request):
00272 data = {}
00273 response = []
00274
00275 def start_response(status, response_headers, exc_info=None):
00276 data["status"] = status
00277 data["headers"] = response_headers
00278 return response.append
00279 app_response = self.wsgi_application(
00280 WSGIContainer.environ(request), start_response)
00281 try:
00282 response.extend(app_response)
00283 body = b"".join(response)
00284 finally:
00285 if hasattr(app_response, "close"):
00286 app_response.close()
00287 if not data:
00288 raise Exception("WSGI app did not call start_response")
00289
00290 status_code = int(data["status"].split()[0])
00291 headers = data["headers"]
00292 header_set = set(k.lower() for (k, v) in headers)
00293 body = escape.utf8(body)
00294 if status_code != 304:
00295 if "content-length" not in header_set:
00296 headers.append(("Content-Length", str(len(body))))
00297 if "content-type" not in header_set:
00298 headers.append(("Content-Type", "text/html; charset=UTF-8"))
00299 if "server" not in header_set:
00300 headers.append(("Server", "TornadoServer/%s" % tornado.version))
00301
00302 parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
00303 for key, value in headers:
00304 parts.append(escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n")
00305 parts.append(b"\r\n")
00306 parts.append(body)
00307 request.write(b"".join(parts))
00308 request.finish()
00309 self._log(status_code, request)
00310
00311 @staticmethod
00312 def environ(request):
00313 """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
00314 """
00315 hostport = request.host.split(":")
00316 if len(hostport) == 2:
00317 host = hostport[0]
00318 port = int(hostport[1])
00319 else:
00320 host = request.host
00321 port = 443 if request.protocol == "https" else 80
00322 environ = {
00323 "REQUEST_METHOD": request.method,
00324 "SCRIPT_NAME": "",
00325 "PATH_INFO": to_wsgi_str(escape.url_unescape(
00326 request.path, encoding=None, plus=False)),
00327 "QUERY_STRING": request.query,
00328 "REMOTE_ADDR": request.remote_ip,
00329 "SERVER_NAME": host,
00330 "SERVER_PORT": str(port),
00331 "SERVER_PROTOCOL": request.version,
00332 "wsgi.version": (1, 0),
00333 "wsgi.url_scheme": request.protocol,
00334 "wsgi.input": BytesIO(escape.utf8(request.body)),
00335 "wsgi.errors": sys.stderr,
00336 "wsgi.multithread": False,
00337 "wsgi.multiprocess": True,
00338 "wsgi.run_once": False,
00339 }
00340 if "Content-Type" in request.headers:
00341 environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
00342 if "Content-Length" in request.headers:
00343 environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
00344 for key, value in request.headers.items():
00345 environ["HTTP_" + key.replace("-", "_").upper()] = value
00346 return environ
00347
00348 def _log(self, status_code, request):
00349 if status_code < 400:
00350 log_method = access_log.info
00351 elif status_code < 500:
00352 log_method = access_log.warning
00353 else:
00354 log_method = access_log.error
00355 request_time = 1000.0 * request.request_time()
00356 summary = request.method + " " + request.uri + " (" + \
00357 request.remote_ip + ")"
00358 log_method("%d %s %.2fms", status_code, summary, request_time)
00359
00360
00361 HTTPRequest = httputil.HTTPServerRequest