Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 283 lines (248 sloc) 11.281 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
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
19 WSGI is the Python standard for web servers, and allows for interoperability
20 between Tornado and other Python web frameworks and servers. This module
21 provides WSGI support in two ways:
22
23 * `WSGIApplication` is a version of `tornado.web.Application` that can run
24 inside a WSGI server. This is useful for running a Tornado app on another
25 HTTP server, such as Google App Engine. See the `WSGIApplication` class
26 documentation for limitations that apply.
27 * `WSGIContainer` lets you run other WSGI applications and frameworks on the
28 Tornado HTTP server. For example, with this class you can mix Django
29 and Tornado handlers in a single server.
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
30 """
31
28adce3 Make all internal imports of tornado modules absolute
Ben Darnell authored
32 import cgi
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
33 import httplib
34 import logging
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
35 import sys
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
36 import time
803f33c @bdarnell Add a tornado.version variable, and use it anywhere we use the current
bdarnell authored
37 import tornado
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
38 import urllib
28adce3 Make all internal imports of tornado modules absolute
Ben Darnell authored
39
40 from tornado import escape
41 from tornado import httputil
42 from tornado import web
41b4af9 @bdarnell Test multipart/form-data parsing in wsgi and fix it for python3
bdarnell authored
43 from tornado.escape import native_str, utf8
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
44 from tornado.util import b
45
46 try:
47 from io import BytesIO # python 3
48 except ImportError:
49 from cStringIO import StringIO as BytesIO # python 2
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
50
51 class WSGIApplication(web.Application):
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
52 """A WSGI equivalent of `tornado.web.Application`.
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
53
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
54 WSGIApplication is very similar to web.Application, except no
55 asynchronous methods are supported (since WSGI does not support
56 non-blocking requests properly). If you call self.flush() or other
57 asynchronous methods in your request handlers running in a
58 WSGIApplication, we throw an exception.
59
60 Example usage::
61
62 import tornado.web
63 import tornado.wsgi
64 import wsgiref.simple_server
65
66 class MainHandler(tornado.web.RequestHandler):
67 def get(self):
68 self.write("Hello, world")
69
70 if __name__ == "__main__":
71 application = tornado.wsgi.WSGIApplication([
72 (r"/", MainHandler),
73 ])
74 server = wsgiref.simple_server.make_server('', 8888, application)
75 server.serve_forever()
76
77 See the 'appengine' demo for an example of using this module to run
78 a Tornado app on Google AppEngine.
79
80 Since no asynchronous methods are available for WSGI applications, the
81 httpclient and auth modules are both not available for WSGI applications.
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
82 We support the same interface, but handlers running in a WSGIApplication
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
83 do not support flush() or asynchronous methods.
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
84 """
85 def __init__(self, handlers=None, default_host="", **settings):
86 web.Application.__init__(self, handlers, default_host, transforms=[],
1150332 @finiteloop Turn on auto-reloading when 'debug' setting is given
finiteloop authored
87 wsgi=True, **settings)
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
88
89 def __call__(self, environ, start_response):
90 handler = web.Application.__call__(self, HTTPRequest(environ))
91 assert handler._finished
92 status = str(handler._status_code) + " " + \
93 httplib.responses[handler._status_code]
94 headers = handler._headers.items()
95 for cookie_dict in getattr(handler, "_new_cookies", []):
96 for cookie in cookie_dict.values():
97 headers.append(("Set-Cookie", cookie.OutputString(None)))
6f7e885 @bdarnell Test WSGIApplication and make it work on python3
bdarnell authored
98 start_response(status,
99 [(native_str(k), native_str(v)) for (k,v) in headers])
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
100 return handler._write_buffer
101
102
103 class HTTPRequest(object):
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
104 """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
105 def __init__(self, environ):
106 """Parses the given WSGI environ to construct the request."""
107 self.method = environ["REQUEST_METHOD"]
108 self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
109 self.path += urllib.quote(environ.get("PATH_INFO", ""))
110 self.uri = self.path
111 self.arguments = {}
112 self.query = environ.get("QUERY_STRING", "")
113 if self.query:
114 self.uri += "?" + self.query
115 arguments = cgi.parse_qs(self.query)
116 for name, values in arguments.iteritems():
117 values = [v for v in values if v]
118 if values: self.arguments[name] = values
119 self.version = "HTTP/1.1"
a7dc5bc Consolidate the various HTTP header dictionary classes into one,
Ben Darnell authored
120 self.headers = httputil.HTTPHeaders()
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
121 if environ.get("CONTENT_TYPE"):
122 self.headers["Content-Type"] = environ["CONTENT_TYPE"]
123 if environ.get("CONTENT_LENGTH"):
8eef796 @bdarnell Convert WSGI content-length variable to an int at the right place
bdarnell authored
124 self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
125 for key in environ:
126 if key.startswith("HTTP_"):
127 self.headers[key[5:].replace("_", "-")] = environ[key]
128 if self.headers.get("Content-Length"):
2f4835a @bdarnell Read a fixed number of bytes for wsgi input, since some wsgi containers
bdarnell authored
129 self.body = environ["wsgi.input"].read(
8eef796 @bdarnell Convert WSGI content-length variable to an int at the right place
bdarnell authored
130 int(self.headers["Content-Length"]))
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
131 else:
132 self.body = ""
133 self.protocol = environ["wsgi.url_scheme"]
134 self.remote_ip = environ.get("REMOTE_ADDR", "")
135 if environ.get("HTTP_HOST"):
136 self.host = environ["HTTP_HOST"]
137 else:
138 self.host = environ["SERVER_NAME"]
139
140 # Parse request body
141 self.files = {}
142 content_type = self.headers.get("Content-Type", "")
143 if content_type.startswith("application/x-www-form-urlencoded"):
144 for name, values in cgi.parse_qs(self.body).iteritems():
145 self.arguments.setdefault(name, []).extend(values)
146 elif content_type.startswith("multipart/form-data"):
2b06840 Improve parsing of multipart/form-data headers.
Ben Darnell authored
147 if 'boundary=' in content_type:
148 boundary = content_type.split('boundary=',1)[1]
4686042 @bdarnell Refactor redundant code out of httpserver.py and wsgi.py to httputil.py
bdarnell authored
149 if boundary:
150 httputil.parse_multipart_form_data(
151 utf8(boundary), self.body, self.arguments, self.files)
2b06840 Improve parsing of multipart/form-data headers.
Ben Darnell authored
152 else:
153 logging.warning("Invalid multipart/form-data")
2afa973 @finiteloop Move Tornado project to Github
finiteloop authored
154
155 self._start_time = time.time()
156 self._finish_time = None
157
158 def supports_http_1_1(self):
159 """Returns True if this request supports HTTP/1.1 semantics"""
160 return self.version == "HTTP/1.1"
161
162 def full_url(self):
163 """Reconstructs the full URL for this request."""
164 return self.protocol + "://" + self.host + self.uri
165
166 def request_time(self):
167 """Returns the amount of time it took for this request to execute."""
168 if self._finish_time is None:
169 return time.time() - self._start_time
170 else:
171 return self._finish_time - self._start_time
172
173
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
174 class WSGIContainer(object):
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
175 r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
176
177 Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
178 run it. For example::
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
179
180 def simple_app(environ, start_response):
181 status = "200 OK"
182 response_headers = [("Content-type", "text/plain")]
183 start_response(status, response_headers)
184 return ["Hello world!\n"]
185
186 container = tornado.wsgi.WSGIContainer(simple_app)
187 http_server = tornado.httpserver.HTTPServer(container)
188 http_server.listen(8888)
189 tornado.ioloop.IOLoop.instance().start()
190
191 This class is intended to let other frameworks (Django, web.py, etc)
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
192 run on the Tornado HTTP server and I/O loop.
193
194 The `tornado.web.FallbackHandler` class is often useful for mixing
195 Tornado and WSGI apps in the same server. See
196 https://github.com/bdarnell/django-tornado-demo for a complete example.
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
197 """
198 def __init__(self, wsgi_application):
199 self.wsgi_application = wsgi_application
200
201 def __call__(self, request):
202 data = {}
5f4413b Return a write method from start_response, as required by the wsgi spec.
Ben Darnell authored
203 response = []
1ae186a @weaver Add exc_info parameter to start_response() in WSGIContainer.
weaver authored
204 def start_response(status, response_headers, exc_info=None):
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
205 data["status"] = status
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
206 data["headers"] = response_headers
5f4413b Return a write method from start_response, as required by the wsgi spec.
Ben Darnell authored
207 return response.append
df0d88e Close wsgi responses correctly - the close method, if present, will
Ben Darnell authored
208 app_response = self.wsgi_application(
209 WSGIContainer.environ(request), start_response)
210 response.extend(app_response)
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
211 body = b("").join(response)
df0d88e Close wsgi responses correctly - the close method, if present, will
Ben Darnell authored
212 if hasattr(app_response, "close"):
213 app_response.close()
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
214 if not data: raise Exception("WSGI app did not call start_response")
215
216 status_code = int(data["status"].split()[0])
217 headers = data["headers"]
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
218 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
219 body = escape.utf8(body)
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
220 if "content-length" not in header_set:
221 headers.append(("Content-Length", str(len(body))))
222 if "content-type" not in header_set:
223 headers.append(("Content-Type", "text/html; charset=UTF-8"))
224 if "server" not in header_set:
803f33c @bdarnell Add a tornado.version variable, and use it anywhere we use the current
bdarnell authored
225 headers.append(("Server", "TornadoServer/%s" % tornado.version))
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
226
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
227 parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
33a587b Don't put wsgi response headers in a dictionary to support repeated
Ben Darnell authored
228 for key, value in headers:
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
229 parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
230 parts.append(b("\r\n"))
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
231 parts.append(body)
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
232 request.write(b("").join(parts))
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
233 request.finish()
234 self._log(status_code, request)
235
400d2c9 Make WSGIContainer._environ public and static, so it can be used to a…
Ben Darnell authored
236 @staticmethod
237 def environ(request):
55d3be1 @bdarnell Run coverage check and fill in the blanks
bdarnell authored
238 """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
239 """
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
240 hostport = request.host.split(":")
241 if len(hostport) == 2:
242 host = hostport[0]
243 port = int(hostport[1])
244 else:
245 host = request.host
246 port = 443 if request.protocol == "https" else 80
247 environ = {
248 "REQUEST_METHOD": request.method,
249 "SCRIPT_NAME": "",
3d64c89 @bdarnell Unquote PATH_INFO in wsgi.
bdarnell authored
250 "PATH_INFO": urllib.unquote(request.path),
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
251 "QUERY_STRING": request.query,
41a9473 Add REMOTE_ADDR to WSGIContainer
Ben Darnell authored
252 "REMOTE_ADDR": request.remote_ip,
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
253 "SERVER_NAME": host,
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
254 "SERVER_PORT": str(port),
f6266ba Add SERVER_PROTOCOL variable to wsgi environment. This turns out to be
Ben Darnell authored
255 "SERVER_PROTOCOL": request.version,
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
256 "wsgi.version": (1, 0),
257 "wsgi.url_scheme": request.protocol,
cab718a @bdarnell Make WSGIContainer work on python 3
bdarnell authored
258 "wsgi.input": BytesIO(escape.utf8(request.body)),
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
259 "wsgi.errors": sys.stderr,
260 "wsgi.multithread": False,
261 "wsgi.multiprocess": True,
262 "wsgi.run_once": False,
263 }
264 if "Content-Type" in request.headers:
41b4af9 @bdarnell Test multipart/form-data parsing in wsgi and fix it for python3
bdarnell authored
265 environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
266 if "Content-Length" in request.headers:
41b4af9 @bdarnell Test multipart/form-data parsing in wsgi and fix it for python3
bdarnell authored
267 environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
268 for key, value in request.headers.iteritems():
269 environ["HTTP_" + key.replace("-", "_").upper()] = value
270 return environ
271
272 def _log(self, status_code, request):
273 if status_code < 400:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
274 log_method = logging.info
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
275 elif status_code < 500:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
276 log_method = logging.warning
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
277 else:
ca8002f Send all logging to the root logger instead of per-module loggers.
Ben Darnell authored
278 log_method = logging.error
8ca6160 @finiteloop Add initial WSGI container support for running other frameworks on To…
finiteloop authored
279 request_time = 1000.0 * request.request_time()
280 summary = request.method + " " + request.uri + " (" + \
281 request.remote_ip + ")"
282 log_method("%d %s %.2fms", status_code, summary, request_time)
Something went wrong with that request. Please try again.