~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/tornado/tornado/wsgi.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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.
 
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.
 
51
"""
 
52
 
 
53
import cgi
 
54
import cStringIO
 
55
import escape
 
56
import httplib
 
57
import logging
 
58
import sys
 
59
import time
 
60
import urllib
 
61
import web
 
62
 
 
63
_log = logging.getLogger('tornado.wsgi')
 
64
 
 
65
class WSGIApplication(web.Application):
 
66
    """A WSGI-equivalent of web.Application.
 
67
 
 
68
    We support the same interface, but handlers running in a WSGIApplication
 
69
    do not support flush() or asynchronous methods.
 
70
    """
 
71
    def __init__(self, handlers=None, default_host="", **settings):
 
72
        web.Application.__init__(self, handlers, default_host, transforms=[],
 
73
                                 wsgi=True, **settings)
 
74
 
 
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
 
86
 
 
87
 
 
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", ""))
 
95
        self.uri = self.path
 
96
        self.arguments = {}
 
97
        self.query = environ.get("QUERY_STRING", "")
 
98
        if self.query:
 
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"])
 
110
        for key in environ:
 
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()
 
115
        else:
 
116
            self.body = ""
 
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"]
 
121
        else:
 
122
            self.host = environ["SERVER_NAME"]
 
123
 
 
124
        # Parse request body
 
125
        self.files = {}
 
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)
 
133
 
 
134
        self._start_time = time.time()
 
135
        self._finish_time = None
 
136
 
 
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"
 
140
 
 
141
    def full_url(self):
 
142
        """Reconstructs the full URL for this request."""
 
143
        return self.protocol + "://" + self.host + self.uri
 
144
 
 
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
 
149
        else:
 
150
            return self._finish_time - self._start_time
 
151
 
 
152
    def _parse_mime_body(self, boundary):
 
153
        if self.body.endswith("\r\n"):
 
154
            footer_length = len(boundary) + 6
 
155
        else:
 
156
            footer_length = len(boundary) + 4
 
157
        parts = self.body[:-footer_length].split("--" + boundary + "\r\n")
 
158
        for part in parts:
 
159
            if not part: continue
 
160
            eoh = part.find("\r\n\r\n")
 
161
            if eoh == -1:
 
162
                _log.warning("multipart/form-data missing headers")
 
163
                continue
 
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")
 
169
                continue
 
170
            value = part[eoh + 4:-2]
 
171
            name_values = {}
 
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")
 
177
                continue
 
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,
 
183
                    content_type=ctype))
 
184
            else:
 
185
                self.arguments.setdefault(name, []).append(value)
 
186
 
 
187
 
 
188
class WSGIContainer(object):
 
189
    """Makes a WSGI-compatible function runnable on Tornado's HTTP server.
 
190
 
 
191
    Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
 
192
    run it. For example:
 
193
 
 
194
        def simple_app(environ, start_response):
 
195
            status = "200 OK"
 
196
            response_headers = [("Content-type", "text/plain")]
 
197
            start_response(status, response_headers)
 
198
            return ["Hello world!\n"]
 
199
 
 
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()
 
204
 
 
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.
 
208
    """
 
209
    def __init__(self, wsgi_application):
 
210
        self.wsgi_application = wsgi_application
 
211
 
 
212
    def __call__(self, request):
 
213
        data = {}
 
214
        response = []
 
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"):
 
223
            response.close()
 
224
        if not data: raise Exception("WSGI app did not call start_response")
 
225
 
 
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"))
 
236
 
 
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")
 
240
        parts.append("\r\n")
 
241
        parts.append(body)
 
242
        request.write("".join(parts))
 
243
        request.finish()
 
244
        self._log(status_code, request)
 
245
 
 
246
    @staticmethod
 
247
    def environ(request):
 
248
        hostport = request.host.split(":")
 
249
        if len(hostport) == 2:
 
250
            host = hostport[0]
 
251
            port = int(hostport[1])
 
252
        else:
 
253
            host = request.host
 
254
            port = 443 if request.protocol == "https" else 80
 
255
        environ = {
 
256
            "REQUEST_METHOD": request.method,
 
257
            "SCRIPT_NAME": "",
 
258
            "PATH_INFO": request.path,
 
259
            "QUERY_STRING": request.query,
 
260
            "REMOTE_ADDR": request.remote_ip,
 
261
            "SERVER_NAME": host,
 
262
            "SERVER_PORT": port,
 
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,
 
271
        }
 
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
 
278
        return environ
 
279
 
 
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
 
285
        else:
 
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)
 
291
 
 
292
 
 
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)
 
297
 
 
298
    def __getitem__(self, name):
 
299
        return dict.__getitem__(self, self._normalize_name(name))
 
300
 
 
301
    def _normalize_name(self, name):
 
302
        return "-".join([w.capitalize() for w in name.split("-")])
 
303
 
 
304
    @classmethod
 
305
    def parse(cls, headers_string):
 
306
        headers = cls()
 
307
        for line in headers_string.splitlines():
 
308
            if line:
 
309
                name, value = line.split(": ", 1)
 
310
                headers[name] = value
 
311
        return headers