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 * `WSGIApplication` is a version of `tornado.web.Application` that can run
00024 inside a WSGI server. This is useful for running a Tornado app on another
00025 HTTP server, such as Google App Engine. See the `WSGIApplication` 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, with_statement
00033
00034 import Cookie
00035 import httplib
00036 import logging
00037 import sys
00038 import time
00039 import tornado
00040 import urllib
00041
00042 from tornado import escape
00043 from tornado import httputil
00044 from tornado import web
00045 from tornado.escape import native_str, utf8, parse_qs_bytes
00046 from tornado.util import b
00047
00048 try:
00049 from io import BytesIO
00050 except ImportError:
00051 from cStringIO import StringIO as BytesIO
00052
00053
00054 class WSGIApplication(web.Application):
00055 """A WSGI equivalent of `tornado.web.Application`.
00056
00057 WSGIApplication is very similar to web.Application, except no
00058 asynchronous methods are supported (since WSGI does not support
00059 non-blocking requests properly). If you call self.flush() or other
00060 asynchronous methods in your request handlers running in a
00061 WSGIApplication, we throw an exception.
00062
00063 Example usage::
00064
00065 import tornado.web
00066 import tornado.wsgi
00067 import wsgiref.simple_server
00068
00069 class MainHandler(tornado.web.RequestHandler):
00070 def get(self):
00071 self.write("Hello, world")
00072
00073 if __name__ == "__main__":
00074 application = tornado.wsgi.WSGIApplication([
00075 (r"/", MainHandler),
00076 ])
00077 server = wsgiref.simple_server.make_server('', 8888, application)
00078 server.serve_forever()
00079
00080 See the 'appengine' demo for an example of using this module to run
00081 a Tornado app on Google AppEngine.
00082
00083 Since no asynchronous methods are available for WSGI applications, the
00084 httpclient and auth modules are both not available for WSGI applications.
00085 We support the same interface, but handlers running in a WSGIApplication
00086 do not support flush() or asynchronous methods.
00087 """
00088 def __init__(self, handlers=None, default_host="", **settings):
00089 web.Application.__init__(self, handlers, default_host, transforms=[],
00090 wsgi=True, **settings)
00091
00092 def __call__(self, environ, start_response):
00093 handler = web.Application.__call__(self, HTTPRequest(environ))
00094 assert handler._finished
00095 status = str(handler._status_code) + " " + \
00096 httplib.responses[handler._status_code]
00097 headers = handler._headers.items()
00098 if hasattr(handler, "_new_cookie"):
00099 for cookie in handler._new_cookie.values():
00100 headers.append(("Set-Cookie", cookie.OutputString(None)))
00101 start_response(status,
00102 [(native_str(k), native_str(v)) for (k, v) in headers])
00103 return handler._write_buffer
00104
00105
00106 class HTTPRequest(object):
00107 """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
00108 def __init__(self, environ):
00109 """Parses the given WSGI environ to construct the request."""
00110 self.method = environ["REQUEST_METHOD"]
00111 self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
00112 self.path += urllib.quote(environ.get("PATH_INFO", ""))
00113 self.uri = self.path
00114 self.arguments = {}
00115 self.query = environ.get("QUERY_STRING", "")
00116 if self.query:
00117 self.uri += "?" + self.query
00118 arguments = parse_qs_bytes(native_str(self.query))
00119 for name, values in arguments.iteritems():
00120 values = [v for v in values if v]
00121 if values:
00122 self.arguments[name] = values
00123 self.version = "HTTP/1.1"
00124 self.headers = httputil.HTTPHeaders()
00125 if environ.get("CONTENT_TYPE"):
00126 self.headers["Content-Type"] = environ["CONTENT_TYPE"]
00127 if environ.get("CONTENT_LENGTH"):
00128 self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
00129 for key in environ:
00130 if key.startswith("HTTP_"):
00131 self.headers[key[5:].replace("_", "-")] = environ[key]
00132 if self.headers.get("Content-Length"):
00133 self.body = environ["wsgi.input"].read(
00134 int(self.headers["Content-Length"]))
00135 else:
00136 self.body = ""
00137 self.protocol = environ["wsgi.url_scheme"]
00138 self.remote_ip = environ.get("REMOTE_ADDR", "")
00139 if environ.get("HTTP_HOST"):
00140 self.host = environ["HTTP_HOST"]
00141 else:
00142 self.host = environ["SERVER_NAME"]
00143
00144
00145 self.files = {}
00146 content_type = self.headers.get("Content-Type", "")
00147 if content_type.startswith("application/x-www-form-urlencoded"):
00148 for name, values in parse_qs_bytes(native_str(self.body)).iteritems():
00149 self.arguments.setdefault(name, []).extend(values)
00150 elif content_type.startswith("multipart/form-data"):
00151 if 'boundary=' in content_type:
00152 boundary = content_type.split('boundary=', 1)[1]
00153 if boundary:
00154 httputil.parse_multipart_form_data(
00155 utf8(boundary), self.body, self.arguments, self.files)
00156 else:
00157 logging.warning("Invalid multipart/form-data")
00158
00159 self._start_time = time.time()
00160 self._finish_time = None
00161
00162 def supports_http_1_1(self):
00163 """Returns True if this request supports HTTP/1.1 semantics"""
00164 return self.version == "HTTP/1.1"
00165
00166 @property
00167 def cookies(self):
00168 """A dictionary of Cookie.Morsel objects."""
00169 if not hasattr(self, "_cookies"):
00170 self._cookies = Cookie.SimpleCookie()
00171 if "Cookie" in self.headers:
00172 try:
00173 self._cookies.load(
00174 native_str(self.headers["Cookie"]))
00175 except Exception:
00176 self._cookies = None
00177 return self._cookies
00178
00179 def full_url(self):
00180 """Reconstructs the full URL for this request."""
00181 return self.protocol + "://" + self.host + self.uri
00182
00183 def request_time(self):
00184 """Returns the amount of time it took for this request to execute."""
00185 if self._finish_time is None:
00186 return time.time() - self._start_time
00187 else:
00188 return self._finish_time - self._start_time
00189
00190
00191 class WSGIContainer(object):
00192 r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
00193
00194 Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
00195 run it. For example::
00196
00197 def simple_app(environ, start_response):
00198 status = "200 OK"
00199 response_headers = [("Content-type", "text/plain")]
00200 start_response(status, response_headers)
00201 return ["Hello world!\n"]
00202
00203 container = tornado.wsgi.WSGIContainer(simple_app)
00204 http_server = tornado.httpserver.HTTPServer(container)
00205 http_server.listen(8888)
00206 tornado.ioloop.IOLoop.instance().start()
00207
00208 This class is intended to let other frameworks (Django, web.py, etc)
00209 run on the Tornado HTTP server and I/O loop.
00210
00211 The `tornado.web.FallbackHandler` class is often useful for mixing
00212 Tornado and WSGI apps in the same server. See
00213 https://github.com/bdarnell/django-tornado-demo for a complete example.
00214 """
00215 def __init__(self, wsgi_application):
00216 self.wsgi_application = wsgi_application
00217
00218 def __call__(self, request):
00219 data = {}
00220 response = []
00221
00222 def start_response(status, response_headers, exc_info=None):
00223 data["status"] = status
00224 data["headers"] = response_headers
00225 return response.append
00226 app_response = self.wsgi_application(
00227 WSGIContainer.environ(request), start_response)
00228 response.extend(app_response)
00229 body = b("").join(response)
00230 if hasattr(app_response, "close"):
00231 app_response.close()
00232 if not data:
00233 raise Exception("WSGI app did not call start_response")
00234
00235 status_code = int(data["status"].split()[0])
00236 headers = data["headers"]
00237 header_set = set(k.lower() for (k, v) in headers)
00238 body = escape.utf8(body)
00239 if "content-length" not in header_set:
00240 headers.append(("Content-Length", str(len(body))))
00241 if "content-type" not in header_set:
00242 headers.append(("Content-Type", "text/html; charset=UTF-8"))
00243 if "server" not in header_set:
00244 headers.append(("Server", "TornadoServer/%s" % tornado.version))
00245
00246 parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
00247 for key, value in headers:
00248 parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
00249 parts.append(b("\r\n"))
00250 parts.append(body)
00251 request.write(b("").join(parts))
00252 request.finish()
00253 self._log(status_code, request)
00254
00255 @staticmethod
00256 def environ(request):
00257 """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
00258 """
00259 hostport = request.host.split(":")
00260 if len(hostport) == 2:
00261 host = hostport[0]
00262 port = int(hostport[1])
00263 else:
00264 host = request.host
00265 port = 443 if request.protocol == "https" else 80
00266 environ = {
00267 "REQUEST_METHOD": request.method,
00268 "SCRIPT_NAME": "",
00269 "PATH_INFO": urllib.unquote(request.path),
00270 "QUERY_STRING": request.query,
00271 "REMOTE_ADDR": request.remote_ip,
00272 "SERVER_NAME": host,
00273 "SERVER_PORT": str(port),
00274 "SERVER_PROTOCOL": request.version,
00275 "wsgi.version": (1, 0),
00276 "wsgi.url_scheme": request.protocol,
00277 "wsgi.input": BytesIO(escape.utf8(request.body)),
00278 "wsgi.errors": sys.stderr,
00279 "wsgi.multithread": False,
00280 "wsgi.multiprocess": True,
00281 "wsgi.run_once": False,
00282 }
00283 if "Content-Type" in request.headers:
00284 environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
00285 if "Content-Length" in request.headers:
00286 environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
00287 for key, value in request.headers.iteritems():
00288 environ["HTTP_" + key.replace("-", "_").upper()] = value
00289 return environ
00290
00291 def _log(self, status_code, request):
00292 if status_code < 400:
00293 log_method = logging.info
00294 elif status_code < 500:
00295 log_method = logging.warning
00296 else:
00297 log_method = logging.error
00298 request_time = 1000.0 * request.request_time()
00299 summary = request.method + " " + request.uri + " (" + \
00300 request.remote_ip + ")"
00301 log_method("%d %s %.2fms", status_code, summary, request_time)