~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/tornado/tornado/httpserver.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
"""A non-blocking, single-threaded HTTP server."""
 
18
 
 
19
import cgi
 
20
import errno
 
21
import functools
 
22
import ioloop
 
23
import iostream
 
24
import logging
 
25
import os
 
26
import socket
 
27
import time
 
28
import urlparse
 
29
 
 
30
try:
 
31
    import fcntl
 
32
except ImportError:
 
33
    if os.name == 'nt':
 
34
        import win32_support as fcntl
 
35
    else:
 
36
        raise
 
37
 
 
38
try:
 
39
    import ssl # Python 2.6+
 
40
except ImportError:
 
41
    ssl = None
 
42
 
 
43
_log = logging.getLogger('tornado.httpserver')
 
44
 
 
45
class HTTPServer(object):
 
46
    """A non-blocking, single-threaded HTTP server.
 
47
 
 
48
    A server is defined by a request callback that takes an HTTPRequest
 
49
    instance as an argument and writes a valid HTTP response with
 
50
    request.write(). request.finish() finishes the request (but does not
 
51
    necessarily close the connection in the case of HTTP/1.1 keep-alive
 
52
    requests). A simple example server that echoes back the URI you
 
53
    requested:
 
54
 
 
55
        import httpserver
 
56
        import ioloop
 
57
 
 
58
        def handle_request(request):
 
59
           message = "You requested %s\n" % request.uri
 
60
           request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
 
61
                         len(message), message))
 
62
           request.finish()
 
63
 
 
64
        http_server = httpserver.HTTPServer(handle_request)
 
65
        http_server.listen(8888)
 
66
        ioloop.IOLoop.instance().start()
 
67
 
 
68
    HTTPServer is a very basic connection handler. Beyond parsing the
 
69
    HTTP request body and headers, the only HTTP semantics implemented
 
70
    in HTTPServer is HTTP/1.1 keep-alive connections. We do not, however,
 
71
    implement chunked encoding, so the request callback must provide a
 
72
    Content-Length header or implement chunked encoding for HTTP/1.1
 
73
    requests for the server to run correctly for HTTP/1.1 clients. If
 
74
    the request handler is unable to do this, you can provide the
 
75
    no_keep_alive argument to the HTTPServer constructor, which will
 
76
    ensure the connection is closed on every request no matter what HTTP
 
77
    version the client is using.
 
78
 
 
79
    If xheaders is True, we support the X-Real-Ip and X-Scheme headers,
 
80
    which override the remote IP and HTTP scheme for all requests. These
 
81
    headers are useful when running Tornado behind a reverse proxy or
 
82
    load balancer.
 
83
 
 
84
    HTTPServer can serve HTTPS (SSL) traffic with Python 2.6+ and OpenSSL.
 
85
    To make this server serve SSL traffic, send the ssl_options dictionary
 
86
    argument with the arguments required for the ssl.wrap_socket() method,
 
87
    including "certfile" and "keyfile":
 
88
 
 
89
       HTTPServer(applicaton, ssl_options={
 
90
           "certfile": os.path.join(data_dir, "mydomain.crt"),
 
91
           "keyfile": os.path.join(data_dir, "mydomain.key"),
 
92
       })
 
93
 
 
94
    By default, listen() runs in a single thread in a single process. You
 
95
    can utilize all available CPUs on this machine by calling bind() and
 
96
    start() instead of listen():
 
97
 
 
98
        http_server = httpserver.HTTPServer(handle_request)
 
99
        http_server.bind(8888)
 
100
        http_server.start() # Forks multiple sub-processes
 
101
        ioloop.IOLoop.instance().start()
 
102
 
 
103
    start() detects the number of CPUs on this machine and "pre-forks" that
 
104
    number of child processes so that we have one Tornado process per CPU,
 
105
    all with their own IOLoop. You can also pass in the specific number of
 
106
    child processes you want to run with if you want to override this
 
107
    auto-detection.
 
108
    """
 
109
    def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
 
110
                 xheaders=False, ssl_options=None):
 
111
        """Initializes the server with the given request callback.
 
112
 
 
113
        If you use pre-forking/start() instead of the listen() method to
 
114
        start your server, you should not pass an IOLoop instance to this
 
115
        constructor. Each pre-forked child process will create its own
 
116
        IOLoop instance after the forking process.
 
117
        """
 
118
        self.request_callback = request_callback
 
119
        self.no_keep_alive = no_keep_alive
 
120
        self.io_loop = io_loop
 
121
        self.xheaders = xheaders
 
122
        self.ssl_options = ssl_options
 
123
        self._socket = None
 
124
        self._started = False
 
125
 
 
126
    def listen(self, port, address=""):
 
127
        """Binds to the given port and starts the server in a single process.
 
128
 
 
129
        This method is a shortcut for:
 
130
 
 
131
            server.bind(port, address)
 
132
            server.start(1)
 
133
 
 
134
        """
 
135
        self.bind(port, address)
 
136
        self.start(1)
 
137
 
 
138
    def bind(self, port, address=""):
 
139
        """Binds this server to the given port on the given IP address.
 
140
 
 
141
        To start the server, call start(). If you want to run this server
 
142
        in a single process, you can call listen() as a shortcut to the
 
143
        sequence of bind() and start() calls.
 
144
        """
 
145
        assert not self._socket
 
146
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
 
147
        flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
 
148
        flags |= fcntl.FD_CLOEXEC
 
149
        fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
 
150
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
151
        self._socket.setblocking(0)
 
152
        self._socket.bind((address, port))
 
153
        self._socket.listen(128)
 
154
 
 
155
    def start(self, num_processes=None):
 
156
        """Starts this server in the IOLoop.
 
157
 
 
158
        By default, we detect the number of cores available on this machine
 
159
        and fork that number of child processes. If num_processes is given, we
 
160
        fork that specific number of sub-processes.
 
161
 
 
162
        If num_processes is 1 or we detect only 1 CPU core, we run the server
 
163
        in this process and do not fork any additional child process.
 
164
 
 
165
        Since we run use processes and not threads, there is no shared memory
 
166
        between any server code.
 
167
        """
 
168
        assert not self._started
 
169
        self._started = True
 
170
        if num_processes is None:
 
171
            # Use sysconf to detect the number of CPUs (cores)
 
172
            try:
 
173
                num_processes = os.sysconf("SC_NPROCESSORS_CONF")
 
174
            except ValueError:
 
175
                _log.error("Could not get num processors from sysconf; "
 
176
                              "running with one process")
 
177
                num_processes = 1
 
178
        if num_processes > 1 and ioloop.IOLoop.initialized():
 
179
            _log.error("Cannot run in multiple processes: IOLoop instance "
 
180
                          "has already been initialized. You cannot call "
 
181
                          "IOLoop.instance() before calling start()")
 
182
            num_processes = 1
 
183
        if num_processes > 1:
 
184
            _log.info("Pre-forking %d server processes", num_processes)
 
185
            for i in range(num_processes):
 
186
                if os.fork() == 0:
 
187
                    self.io_loop = ioloop.IOLoop.instance()
 
188
                    self.io_loop.add_handler(
 
189
                        self._socket.fileno(), self._handle_events,
 
190
                        ioloop.IOLoop.READ)
 
191
                    return
 
192
            os.waitpid(-1, 0)
 
193
        else:
 
194
            if not self.io_loop:
 
195
                self.io_loop = ioloop.IOLoop.instance()
 
196
            self.io_loop.add_handler(self._socket.fileno(),
 
197
                                     self._handle_events,
 
198
                                     ioloop.IOLoop.READ)
 
199
 
 
200
    def stop(self):
 
201
      self.io_loop.remove_handler(self._socket.fileno())
 
202
      self._socket.close()
 
203
 
 
204
    def _handle_events(self, fd, events):
 
205
        while True:
 
206
            try:
 
207
                connection, address = self._socket.accept()
 
208
            except socket.error, e:
 
209
                if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
 
210
                    return
 
211
                raise
 
212
            if self.ssl_options is not None:
 
213
                assert ssl, "Python 2.6+ and OpenSSL required for SSL"
 
214
                connection = ssl.wrap_socket(
 
215
                    connection, server_side=True, **self.ssl_options)
 
216
            try:
 
217
                stream = iostream.IOStream(connection, io_loop=self.io_loop)
 
218
                HTTPConnection(stream, address, self.request_callback,
 
219
                               self.no_keep_alive, self.xheaders)
 
220
            except:
 
221
                _log.error("Error in connection callback", exc_info=True)
 
222
 
 
223
 
 
224
class HTTPConnection(object):
 
225
    """Handles a connection to an HTTP client, executing HTTP requests.
 
226
 
 
227
    We parse HTTP headers and bodies, and execute the request callback
 
228
    until the HTTP conection is closed.
 
229
    """
 
230
    def __init__(self, stream, address, request_callback, no_keep_alive=False,
 
231
                 xheaders=False):
 
232
        self.stream = stream
 
233
        self.address = address
 
234
        self.request_callback = request_callback
 
235
        self.no_keep_alive = no_keep_alive
 
236
        self.xheaders = xheaders
 
237
        self._request = None
 
238
        self._request_finished = False
 
239
        self.stream.read_until("\r\n\r\n", self._on_headers)
 
240
 
 
241
    def write(self, chunk):
 
242
        assert self._request, "Request closed"
 
243
        if not self.stream.closed():
 
244
            self.stream.write(chunk, self._on_write_complete)
 
245
 
 
246
    def finish(self):
 
247
        assert self._request, "Request closed"
 
248
        self._request_finished = True
 
249
        if not self.stream.writing():
 
250
            self._finish_request()
 
251
 
 
252
    def _on_write_complete(self):
 
253
        if self._request_finished:
 
254
            self._finish_request()
 
255
 
 
256
    def _finish_request(self):
 
257
        if self.no_keep_alive:
 
258
            disconnect = True
 
259
        else:
 
260
            connection_header = self._request.headers.get("Connection")
 
261
            if self._request.supports_http_1_1():
 
262
                disconnect = connection_header == "close"
 
263
            elif ("Content-Length" in self._request.headers
 
264
                    or self._request.method in ("HEAD", "GET")):
 
265
                disconnect = connection_header != "Keep-Alive"
 
266
            else:
 
267
                disconnect = True
 
268
        self._request = None
 
269
        self._request_finished = False
 
270
        if disconnect:
 
271
            self.stream.close()
 
272
            return
 
273
        self.stream.read_until("\r\n\r\n", self._on_headers)
 
274
 
 
275
    def _on_headers(self, data):
 
276
        eol = data.find("\r\n")
 
277
        start_line = data[:eol]
 
278
        method, uri, version = start_line.split(" ")
 
279
        if not version.startswith("HTTP/"):
 
280
            raise Exception("Malformed HTTP version in HTTP Request-Line")
 
281
        headers = HTTPHeaders.parse(data[eol:])
 
282
        self._request = HTTPRequest(
 
283
            connection=self, method=method, uri=uri, version=version,
 
284
            headers=headers, remote_ip=self.address[0])
 
285
 
 
286
        content_length = headers.get("Content-Length")
 
287
        if content_length:
 
288
            content_length = int(content_length)
 
289
            if content_length > self.stream.max_buffer_size:
 
290
                raise Exception("Content-Length too long")
 
291
            if headers.get("Expect") == "100-continue":
 
292
                self.stream.write("HTTP/1.1 100 (Continue)\r\n\r\n")
 
293
            self.stream.read_bytes(content_length, self._on_request_body)
 
294
            return
 
295
 
 
296
        self.request_callback(self._request)
 
297
 
 
298
    def _on_request_body(self, data):
 
299
        self._request.body = data
 
300
        content_type = self._request.headers.get("Content-Type", "")
 
301
        if self._request.method == "POST":
 
302
            if content_type.startswith("application/x-www-form-urlencoded"):
 
303
                arguments = cgi.parse_qs(self._request.body)
 
304
                for name, values in arguments.iteritems():
 
305
                    values = [v for v in values if v]
 
306
                    if values:
 
307
                        self._request.arguments.setdefault(name, []).extend(
 
308
                            values)
 
309
            elif content_type.startswith("multipart/form-data"):
 
310
                boundary = content_type[30:]
 
311
                if boundary: self._parse_mime_body(boundary, data)
 
312
        self.request_callback(self._request)
 
313
 
 
314
    def _parse_mime_body(self, boundary, data):
 
315
        if data.endswith("\r\n"):
 
316
            footer_length = len(boundary) + 6
 
317
        else:
 
318
            footer_length = len(boundary) + 4
 
319
        parts = data[:-footer_length].split("--" + boundary + "\r\n")
 
320
        for part in parts:
 
321
            if not part: continue
 
322
            eoh = part.find("\r\n\r\n")
 
323
            if eoh == -1:
 
324
                _log.warning("multipart/form-data missing headers")
 
325
                continue
 
326
            headers = HTTPHeaders.parse(part[:eoh])
 
327
            name_header = headers.get("Content-Disposition", "")
 
328
            if not name_header.startswith("form-data;") or \
 
329
               not part.endswith("\r\n"):
 
330
                _log.warning("Invalid multipart/form-data")
 
331
                continue
 
332
            value = part[eoh + 4:-2]
 
333
            name_values = {}
 
334
            for name_part in name_header[10:].split(";"):
 
335
                name, name_value = name_part.strip().split("=", 1)
 
336
                name_values[name] = name_value.strip('"').decode("utf-8")
 
337
            if not name_values.get("name"):
 
338
                _log.warning("multipart/form-data value missing name")
 
339
                continue
 
340
            name = name_values["name"]
 
341
            if name_values.get("filename"):
 
342
                ctype = headers.get("Content-Type", "application/unknown")
 
343
                self._request.files.setdefault(name, []).append(dict(
 
344
                    filename=name_values["filename"], body=value,
 
345
                    content_type=ctype))
 
346
            else:
 
347
                self._request.arguments.setdefault(name, []).append(value)
 
348
 
 
349
 
 
350
class HTTPRequest(object):
 
351
    """A single HTTP request.
 
352
 
 
353
    GET/POST arguments are available in the arguments property, which
 
354
    maps arguments names to lists of values (to support multiple values
 
355
    for individual names). Names and values are both unicode always.
 
356
 
 
357
    File uploads are available in the files property, which maps file
 
358
    names to list of files. Each file is a dictionary of the form
 
359
    {"filename":..., "content_type":..., "body":...}. The content_type
 
360
    comes from the provided HTTP header and should not be trusted
 
361
    outright given that it can be easily forged.
 
362
 
 
363
    An HTTP request is attached to a single HTTP connection, which can
 
364
    be accessed through the "connection" attribute. Since connections
 
365
    are typically kept open in HTTP/1.1, multiple requests can be handled
 
366
    sequentially on a single connection.
 
367
    """
 
368
    def __init__(self, method, uri, version="HTTP/1.0", headers=None,
 
369
                 body=None, remote_ip=None, protocol=None, host=None,
 
370
                 files=None, connection=None):
 
371
        self.method = method
 
372
        self.uri = uri
 
373
        self.version = version
 
374
        self.headers = headers or HTTPHeaders()
 
375
        self.body = body or ""
 
376
        if connection and connection.xheaders:
 
377
            # Squid uses X-Forwarded-For, others use X-Real-Ip
 
378
            self.remote_ip = self.headers.get(
 
379
                "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
 
380
            self.protocol = self.headers.get("X-Scheme", protocol) or "http"
 
381
        else:
 
382
            self.remote_ip = remote_ip
 
383
            self.protocol = protocol or "http"
 
384
        self.host = host or self.headers.get("Host") or "127.0.0.1"
 
385
        self.files = files or {}
 
386
        self.connection = connection
 
387
        self._start_time = time.time()
 
388
        self._finish_time = None
 
389
 
 
390
        scheme, netloc, path, query, fragment = urlparse.urlsplit(uri)
 
391
        self.path = path
 
392
        self.query = query
 
393
        arguments = cgi.parse_qs(query)
 
394
        self.arguments = {}
 
395
        for name, values in arguments.iteritems():
 
396
            values = [v for v in values if v]
 
397
            if values: self.arguments[name] = values
 
398
 
 
399
    def supports_http_1_1(self):
 
400
        """Returns True if this request supports HTTP/1.1 semantics"""
 
401
        return self.version == "HTTP/1.1"
 
402
 
 
403
    def write(self, chunk):
 
404
        """Writes the given chunk to the response stream."""
 
405
        assert isinstance(chunk, str)
 
406
        self.connection.write(chunk)
 
407
 
 
408
    def finish(self):
 
409
        """Finishes this HTTP request on the open connection."""
 
410
        self.connection.finish()
 
411
        self._finish_time = time.time()
 
412
 
 
413
    def full_url(self):
 
414
        """Reconstructs the full URL for this request."""
 
415
        return self.protocol + "://" + self.host + self.uri
 
416
 
 
417
    def request_time(self):
 
418
        """Returns the amount of time it took for this request to execute."""
 
419
        if self._finish_time is None:
 
420
            return time.time() - self._start_time
 
421
        else:
 
422
            return self._finish_time - self._start_time
 
423
 
 
424
    def __repr__(self):
 
425
        attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
 
426
                 "remote_ip", "body")
 
427
        args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
 
428
        return "%s(%s, headers=%s)" % (
 
429
            self.__class__.__name__, args, dict(self.headers))
 
430
 
 
431
 
 
432
class HTTPHeaders(dict):
 
433
    """A dictionary that maintains Http-Header-Case for all keys."""
 
434
    def __setitem__(self, name, value):
 
435
        dict.__setitem__(self, self._normalize_name(name), value)
 
436
 
 
437
    def __getitem__(self, name):
 
438
        return dict.__getitem__(self, self._normalize_name(name))
 
439
 
 
440
    def _normalize_name(self, name):
 
441
        return "-".join([w.capitalize() for w in name.split("-")])
 
442
 
 
443
    @classmethod
 
444
    def parse(cls, headers_string):
 
445
        headers = cls()
 
446
        for line in headers_string.splitlines():
 
447
            if line:
 
448
                name, value = line.split(": ", 1)
 
449
                headers[name] = value
 
450
        return headers