3
# Copyright 2009 Facebook
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
9
# http://www.apache.org/licenses/LICENSE-2.0
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
17
"""WSGI support for the Tornado web framework.
19
We export WSGIApplication, which is very similar to web.Application, except
20
no asynchronous methods are supported (since WSGI does not support
21
non-blocking requests properly). If you call self.flush() or other
22
asynchronous methods in your request handlers running in a WSGIApplication,
23
we throw an exception.
29
import wsgiref.simple_server
31
class MainHandler(tornado.web.RequestHandler):
33
self.write("Hello, world")
35
if __name__ == "__main__":
36
application = tornado.wsgi.WSGIApplication([
39
server = wsgiref.simple_server.make_server('', 8888, application)
40
server.serve_forever()
42
See the 'appengine' demo for an example of using this module to run
43
a Tornado app on Google AppEngine.
45
Since no asynchronous methods are available for WSGI applications, the
46
httpclient and auth modules are both not available for WSGI applications.
48
We also export WSGIContainer, which lets you run other WSGI-compatible
49
frameworks on the Tornado HTTP server and I/O loop. See WSGIContainer for
50
details and documentation.
63
_log = logging.getLogger('tornado.wsgi')
65
class WSGIApplication(web.Application):
66
"""A WSGI-equivalent of web.Application.
68
We support the same interface, but handlers running in a WSGIApplication
69
do not support flush() or asynchronous methods.
71
def __init__(self, handlers=None, default_host="", **settings):
72
web.Application.__init__(self, handlers, default_host, transforms=[],
73
wsgi=True, **settings)
75
def __call__(self, environ, start_response):
76
handler = web.Application.__call__(self, HTTPRequest(environ))
77
assert handler._finished
78
status = str(handler._status_code) + " " + \
79
httplib.responses[handler._status_code]
80
headers = handler._headers.items()
81
for cookie_dict in getattr(handler, "_new_cookies", []):
82
for cookie in cookie_dict.values():
83
headers.append(("Set-Cookie", cookie.OutputString(None)))
84
start_response(status, headers)
85
return handler._write_buffer
88
class HTTPRequest(object):
89
"""Mimics httpserver.HTTPRequest for WSGI applications."""
90
def __init__(self, environ):
91
"""Parses the given WSGI environ to construct the request."""
92
self.method = environ["REQUEST_METHOD"]
93
self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
94
self.path += urllib.quote(environ.get("PATH_INFO", ""))
97
self.query = environ.get("QUERY_STRING", "")
99
self.uri += "?" + self.query
100
arguments = cgi.parse_qs(self.query)
101
for name, values in arguments.iteritems():
102
values = [v for v in values if v]
103
if values: self.arguments[name] = values
104
self.version = "HTTP/1.1"
105
self.headers = HTTPHeaders()
106
if environ.get("CONTENT_TYPE"):
107
self.headers["Content-Type"] = environ["CONTENT_TYPE"]
108
if environ.get("CONTENT_LENGTH"):
109
self.headers["Content-Length"] = int(environ["CONTENT_LENGTH"])
111
if key.startswith("HTTP_"):
112
self.headers[key[5:].replace("_", "-")] = environ[key]
113
if self.headers.get("Content-Length"):
114
self.body = environ["wsgi.input"].read()
117
self.protocol = environ["wsgi.url_scheme"]
118
self.remote_ip = environ.get("REMOTE_ADDR", "")
119
if environ.get("HTTP_HOST"):
120
self.host = environ["HTTP_HOST"]
122
self.host = environ["SERVER_NAME"]
126
content_type = self.headers.get("Content-Type", "")
127
if content_type.startswith("application/x-www-form-urlencoded"):
128
for name, values in cgi.parse_qs(self.body).iteritems():
129
self.arguments.setdefault(name, []).extend(values)
130
elif content_type.startswith("multipart/form-data"):
131
boundary = content_type[30:]
132
if boundary: self._parse_mime_body(boundary)
134
self._start_time = time.time()
135
self._finish_time = None
137
def supports_http_1_1(self):
138
"""Returns True if this request supports HTTP/1.1 semantics"""
139
return self.version == "HTTP/1.1"
142
"""Reconstructs the full URL for this request."""
143
return self.protocol + "://" + self.host + self.uri
145
def request_time(self):
146
"""Returns the amount of time it took for this request to execute."""
147
if self._finish_time is None:
148
return time.time() - self._start_time
150
return self._finish_time - self._start_time
152
def _parse_mime_body(self, boundary):
153
if self.body.endswith("\r\n"):
154
footer_length = len(boundary) + 6
156
footer_length = len(boundary) + 4
157
parts = self.body[:-footer_length].split("--" + boundary + "\r\n")
159
if not part: continue
160
eoh = part.find("\r\n\r\n")
162
_log.warning("multipart/form-data missing headers")
164
headers = HTTPHeaders.parse(part[:eoh])
165
name_header = headers.get("Content-Disposition", "")
166
if not name_header.startswith("form-data;") or \
167
not part.endswith("\r\n"):
168
_log.warning("Invalid multipart/form-data")
170
value = part[eoh + 4:-2]
172
for name_part in name_header[10:].split(";"):
173
name, name_value = name_part.strip().split("=", 1)
174
name_values[name] = name_value.strip('"').decode("utf-8")
175
if not name_values.get("name"):
176
_log.warning("multipart/form-data value missing name")
178
name = name_values["name"]
179
if name_values.get("filename"):
180
ctype = headers.get("Content-Type", "application/unknown")
181
self.files.setdefault(name, []).append(dict(
182
filename=name_values["filename"], body=value,
185
self.arguments.setdefault(name, []).append(value)
188
class WSGIContainer(object):
189
"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
191
Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
194
def simple_app(environ, start_response):
196
response_headers = [("Content-type", "text/plain")]
197
start_response(status, response_headers)
198
return ["Hello world!\n"]
200
container = tornado.wsgi.WSGIContainer(simple_app)
201
http_server = tornado.httpserver.HTTPServer(container)
202
http_server.listen(8888)
203
tornado.ioloop.IOLoop.instance().start()
205
This class is intended to let other frameworks (Django, web.py, etc)
206
run on the Tornado HTTP server and I/O loop. It has not yet been
207
thoroughly tested in production.
209
def __init__(self, wsgi_application):
210
self.wsgi_application = wsgi_application
212
def __call__(self, request):
215
def start_response(status, response_headers, exc_info=None):
216
data["status"] = status
217
data["headers"] = response_headers
218
return response.append
219
response.extend(self.wsgi_application(
220
WSGIContainer.environ(request), start_response))
221
body = "".join(response)
222
if hasattr(response, "close"):
224
if not data: raise Exception("WSGI app did not call start_response")
226
status_code = int(data["status"].split()[0])
227
headers = data["headers"]
228
header_set = set(k.lower() for (k,v) in headers)
229
body = escape.utf8(body)
230
if "content-length" not in header_set:
231
headers.append(("Content-Length", str(len(body))))
232
if "content-type" not in header_set:
233
headers.append(("Content-Type", "text/html; charset=UTF-8"))
234
if "server" not in header_set:
235
headers.append(("Server", "TornadoServer/0.1"))
237
parts = ["HTTP/1.1 " + data["status"] + "\r\n"]
238
for key, value in headers:
239
parts.append(escape.utf8(key) + ": " + escape.utf8(value) + "\r\n")
242
request.write("".join(parts))
244
self._log(status_code, request)
247
def environ(request):
248
hostport = request.host.split(":")
249
if len(hostport) == 2:
251
port = int(hostport[1])
254
port = 443 if request.protocol == "https" else 80
256
"REQUEST_METHOD": request.method,
258
"PATH_INFO": request.path,
259
"QUERY_STRING": request.query,
260
"REMOTE_ADDR": request.remote_ip,
263
"SERVER_PROTOCOL": request.version,
264
"wsgi.version": (1, 0),
265
"wsgi.url_scheme": request.protocol,
266
"wsgi.input": cStringIO.StringIO(request.body),
267
"wsgi.errors": sys.stderr,
268
"wsgi.multithread": False,
269
"wsgi.multiprocess": True,
270
"wsgi.run_once": False,
272
if "Content-Type" in request.headers:
273
environ["CONTENT_TYPE"] = request.headers["Content-Type"]
274
if "Content-Length" in request.headers:
275
environ["CONTENT_LENGTH"] = request.headers["Content-Length"]
276
for key, value in request.headers.iteritems():
277
environ["HTTP_" + key.replace("-", "_").upper()] = value
280
def _log(self, status_code, request):
281
if status_code < 400:
282
log_method = _log.info
283
elif status_code < 500:
284
log_method = _log.warning
286
log_method = _log.error
287
request_time = 1000.0 * request.request_time()
288
summary = request.method + " " + request.uri + " (" + \
289
request.remote_ip + ")"
290
log_method("%d %s %.2fms", status_code, summary, request_time)
293
class HTTPHeaders(dict):
294
"""A dictionary that maintains Http-Header-Case for all keys."""
295
def __setitem__(self, name, value):
296
dict.__setitem__(self, self._normalize_name(name), value)
298
def __getitem__(self, name):
299
return dict.__getitem__(self, self._normalize_name(name))
301
def _normalize_name(self, name):
302
return "-".join([w.capitalize() for w in name.split("-")])
305
def parse(cls, headers_string):
307
for line in headers_string.splitlines():
309
name, value = line.split(": ", 1)
310
headers[name] = value