Skip to content
Newer
Older
100644 298 lines (264 sloc) 11.5 KB
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
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
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.
24
25 Example usage:
26
27 import tornado.web
28 import tornado.wsgi
29 import wsgiref.simple_server
30
31 class MainHandler(tornado.web.RequestHandler):
32 def get(self):
33 self.write("Hello, world")
34
35 if __name__ == "__main__":
36 application = tornado.wsgi.WSGIApplication([
37 (r"/", MainHandler),
38 ])
39 server = wsgiref.simple_server.make_server('', 8888, application)
40 server.serve_forever()
41
42 See the 'appengine' demo for an example of using this module to run
43 a Tornado app on Google AppEngine.
44
45 Since no asynchronous methods are available for WSGI applications, the
46 httpclient and auth modules are both not available for WSGI applications.
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
47
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.
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
51 """
52
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
53 import cStringIO
28adce3 Make all internal imports of tornado modules absolute
Ben Darnell authored
54 import cgi
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
55 import httplib
56 import logging
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
57 import sys
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
58 import time
803f33c @bdarnell Add a tornado.version variable, and use it anywhere we use the current
bdarnell authored
59 import tornado
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
60 import urllib
28adce3 Make all internal imports of tornado modules absolute
Ben Darnell authored
61
62 from tornado import escape
63 from tornado import httputil
64 from tornado import web
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
65
66 class WSGIApplication(web.Application):
67 """A WSGI-equivalent of web.Application.
68
69 We support the same interface, but handlers running in a WSGIApplication
70 do not support flush() or asynchronous methods.
71 """
72 def __init__(self, handlers=None, default_host="", **settings):
73 web.Application.__init__(self, handlers, default_host, transforms=[],
1150332 @finiteloop Turn on auto-reloading when 'debug' setting is given
finiteloop authored
74 wsgi=True, **settings)
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
75
76 def __call__(self, environ, start_response):
77 handler = web.Application.__call__(self, HTTPRequest(environ))
78 assert handler._finished
79 status = str(handler._status_code) + " " + \
80 httplib.responses[handler._status_code]
81 headers = handler._headers.items()
82 for cookie_dict in getattr(handler, "_new_cookies", []):
83 for cookie in cookie_dict.values():
84 headers.append(("Set-Cookie", cookie.OutputString(None)))
85 start_response(status, headers)
86 return handler._write_buffer
87
88
89 class HTTPRequest(object):
90 """Mimics httpserver.HTTPRequest for WSGI applications."""
91 def __init__(self, environ):
92 """Parses the given WSGI environ to construct the request."""
93 self.method = environ["REQUEST_METHOD"]
94 self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
95 self.path += urllib.quote(environ.get("PATH_INFO", ""))
96 self.uri = self.path
97 self.arguments = {}
98 self.query = environ.get("QUERY_STRING", "")
99 if self.query:
100 self.uri += "?" + self.query
101 arguments = cgi.parse_qs(self.query)
102 for name, values in arguments.iteritems():
103 values = [v for v in values if v]
104 if values: self.arguments[name] = values
105 self.version = "HTTP/1.1"
a7dc5bc Consolidate the various HTTP header dictionary classes into one,
Ben Darnell authored
106 self.headers = httputil.HTTPHeaders()
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
107 if environ.get("CONTENT_TYPE"):
108 self.headers["Content-Type"] = environ["CONTENT_TYPE"]
109 if environ.get("CONTENT_LENGTH"):
110 self.headers["Content-Length"] = int(environ["CONTENT_LENGTH"])
111 for key in environ:
112 if key.startswith("HTTP_"):
113 self.headers[key[5:].replace("_", "-")] = environ[key]
114 if self.headers.get("Content-Length"):
115 self.body = environ["wsgi.input"].read()
116 else:
117 self.body = ""
118 self.protocol = environ["wsgi.url_scheme"]
119 self.remote_ip = environ.get("REMOTE_ADDR", "")
120 if environ.get("HTTP_HOST"):
121 self.host = environ["HTTP_HOST"]
122 else:
123 self.host = environ["SERVER_NAME"]
124
125 # Parse request body
126 self.files = {}
127 content_type = self.headers.get("Content-Type", "")
128 if content_type.startswith("application/x-www-form-urlencoded"):
129 for name, values in cgi.parse_qs(self.body).iteritems():
130 self.arguments.setdefault(name, []).extend(values)
131 elif content_type.startswith("multipart/form-data"):
2b06840 Improve parsing of multipart/form-data headers.
Ben Darnell authored
132 if 'boundary=' in content_type:
133 boundary = content_type.split('boundary=',1)[1]
134 if boundary: self._parse_mime_body(boundary)
135 else:
136 logging.warning("Invalid multipart/form-data")
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
137
138 self._start_time = time.time()
139 self._finish_time = None
140
141 def supports_http_1_1(self):
142 """Returns True if this request supports HTTP/1.1 semantics"""
143 return self.version == "HTTP/1.1"
144
145 def full_url(self):
146 """Reconstructs the full URL for this request."""
147 return self.protocol + "://" + self.host + self.uri
148
149 def request_time(self):
150 """Returns the amount of time it took for this request to execute."""
151 if self._finish_time is None:
152 return time.time() - self._start_time
153 else:
154 return self._finish_time - self._start_time
155
156 def _parse_mime_body(self, boundary):
2b06840 Improve parsing of multipart/form-data headers.
Ben Darnell authored
157 if boundary.startswith('"') and boundary.endswith('"'):
158 boundary = boundary[1:-1]
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
159 if self.body.endswith("\r\n"):
160 footer_length = len(boundary) + 6
161 else:
162 footer_length = len(boundary) + 4
163 parts = self.body[:-footer_length].split("--" + boundary + "\r\n")
164 for part in parts:
165 if not part: continue
166 eoh = part.find("\r\n\r\n")
167 if eoh == -1:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
168 logging.warning("multipart/form-data missing headers")
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
169 continue
a7dc5bc Consolidate the various HTTP header dictionary classes into one,
Ben Darnell authored
170 headers = httputil.HTTPHeaders.parse(part[:eoh])
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
171 name_header = headers.get("Content-Disposition", "")
172 if not name_header.startswith("form-data;") or \
173 not part.endswith("\r\n"):
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
174 logging.warning("Invalid multipart/form-data")
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
175 continue
176 value = part[eoh + 4:-2]
177 name_values = {}
178 for name_part in name_header[10:].split(";"):
179 name, name_value = name_part.strip().split("=", 1)
180 name_values[name] = name_value.strip('"').decode("utf-8")
181 if not name_values.get("name"):
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
182 logging.warning("multipart/form-data value missing name")
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
183 continue
184 name = name_values["name"]
185 if name_values.get("filename"):
186 ctype = headers.get("Content-Type", "application/unknown")
187 self.files.setdefault(name, []).append(dict(
188 filename=name_values["filename"], body=value,
189 content_type=ctype))
190 else:
191 self.arguments.setdefault(name, []).append(value)
192
193
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
194 class WSGIContainer(object):
195 """Makes a WSGI-compatible function runnable on Tornado's HTTP server.
196
197 Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
198 run it. For example:
199
200 def simple_app(environ, start_response):
201 status = "200 OK"
202 response_headers = [("Content-type", "text/plain")]
203 start_response(status, response_headers)
204 return ["Hello world!\n"]
205
206 container = tornado.wsgi.WSGIContainer(simple_app)
207 http_server = tornado.httpserver.HTTPServer(container)
208 http_server.listen(8888)
209 tornado.ioloop.IOLoop.instance().start()
210
211 This class is intended to let other frameworks (Django, web.py, etc)
212 run on the Tornado HTTP server and I/O loop. It has not yet been
213 thoroughly tested in production.
214 """
215 def __init__(self, wsgi_application):
216 self.wsgi_application = wsgi_application
217
218 def __call__(self, request):
219 data = {}
5f4413b Return a write method from start_response, as required by the wsgi spec.
Ben Darnell authored
220 response = []
1ae186a @weaver Add exc_info parameter to start_response() in WSGIContainer.
weaver authored
221 def start_response(status, response_headers, exc_info=None):
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
222 data["status"] = status
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
223 data["headers"] = response_headers
5f4413b Return a write method from start_response, as required by the wsgi spec.
Ben Darnell authored
224 return response.append
df0d88e Close wsgi responses correctly - the close method, if present, will
Ben Darnell authored
225 app_response = self.wsgi_application(
226 WSGIContainer.environ(request), start_response)
227 response.extend(app_response)
61f0faf Call the close() method on the wsgi response object if it exists.
Ben Darnell authored
228 body = "".join(response)
df0d88e Close wsgi responses correctly - the close method, if present, will
Ben Darnell authored
229 if hasattr(app_response, "close"):
230 app_response.close()
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
231 if not data: raise Exception("WSGI app did not call start_response")
232
233 status_code = int(data["status"].split()[0])
234 headers = data["headers"]
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
235 header_set = set(k.lower() for (k,v) in headers)
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
236 body = escape.utf8(body)
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
237 if "content-length" not in header_set:
238 headers.append(("Content-Length", str(len(body))))
239 if "content-type" not in header_set:
240 headers.append(("Content-Type", "text/html; charset=UTF-8"))
241 if "server" not in header_set:
803f33c @bdarnell Add a tornado.version variable, and use it anywhere we use the current
bdarnell authored
242 headers.append(("Server", "TornadoServer/%s" % tornado.version))
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
243
244 parts = ["HTTP/1.1 " + data["status"] + "\r\n"]
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
245 for key, value in headers:
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
246 parts.append(escape.utf8(key) + ": " + escape.utf8(value) + "\r\n")
247 parts.append("\r\n")
248 parts.append(body)
249 request.write("".join(parts))
250 request.finish()
251 self._log(status_code, request)
252
400d2c9 Make WSGIContainer._environ public and static, so it can be used to a…
Ben Darnell authored
253 @staticmethod
254 def environ(request):
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
255 hostport = request.host.split(":")
256 if len(hostport) == 2:
257 host = hostport[0]
258 port = int(hostport[1])
259 else:
260 host = request.host
261 port = 443 if request.protocol == "https" else 80
262 environ = {
263 "REQUEST_METHOD": request.method,
264 "SCRIPT_NAME": "",
265 "PATH_INFO": request.path,
266 "QUERY_STRING": request.query,
41a9473 Add REMOTE_ADDR to WSGIContainer
Ben Darnell authored
267 "REMOTE_ADDR": request.remote_ip,
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
268 "SERVER_NAME": host,
269 "SERVER_PORT": port,
f6266ba Add SERVER_PROTOCOL variable to wsgi environment. This turns out to be
Ben Darnell authored
270 "SERVER_PROTOCOL": request.version,
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
271 "wsgi.version": (1, 0),
272 "wsgi.url_scheme": request.protocol,
9f7c9a3 Use escape.utf8() instead of .encode('utf-8') so we don't double-encode
Ben Darnell authored
273 "wsgi.input": cStringIO.StringIO(escape.utf8(request.body)),
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
274 "wsgi.errors": sys.stderr,
275 "wsgi.multithread": False,
276 "wsgi.multiprocess": True,
277 "wsgi.run_once": False,
278 }
279 if "Content-Type" in request.headers:
280 environ["CONTENT_TYPE"] = request.headers["Content-Type"]
281 if "Content-Length" in request.headers:
282 environ["CONTENT_LENGTH"] = request.headers["Content-Length"]
283 for key, value in request.headers.iteritems():
284 environ["HTTP_" + key.replace("-", "_").upper()] = value
285 return environ
286
287 def _log(self, status_code, request):
288 if status_code < 400:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
289 log_method = logging.info
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
290 elif status_code < 500:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
291 log_method = logging.warning
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
292 else:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
293 log_method = logging.error
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
294 request_time = 1000.0 * request.request_time()
295 summary = request.method + " " + request.uri + " (" + \
296 request.remote_ip + ")"
297 log_method("%d %s %.2fms", status_code, summary, request_time)
Something went wrong with that request. Please try again.