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
"""A non-blocking, single-threaded HTTP server."""
34
import win32_support as fcntl
39
import ssl # Python 2.6+
43
_log = logging.getLogger('tornado.httpserver')
45
class HTTPServer(object):
46
"""A non-blocking, single-threaded HTTP server.
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
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))
64
http_server = httpserver.HTTPServer(handle_request)
65
http_server.listen(8888)
66
ioloop.IOLoop.instance().start()
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.
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
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":
89
HTTPServer(applicaton, ssl_options={
90
"certfile": os.path.join(data_dir, "mydomain.crt"),
91
"keyfile": os.path.join(data_dir, "mydomain.key"),
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():
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()
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
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.
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.
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
124
self._started = False
126
def listen(self, port, address=""):
127
"""Binds to the given port and starts the server in a single process.
129
This method is a shortcut for:
131
server.bind(port, address)
135
self.bind(port, address)
138
def bind(self, port, address=""):
139
"""Binds this server to the given port on the given IP address.
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.
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)
155
def start(self, num_processes=None):
156
"""Starts this server in the IOLoop.
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.
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.
165
Since we run use processes and not threads, there is no shared memory
166
between any server code.
168
assert not self._started
170
if num_processes is None:
171
# Use sysconf to detect the number of CPUs (cores)
173
num_processes = os.sysconf("SC_NPROCESSORS_CONF")
175
_log.error("Could not get num processors from sysconf; "
176
"running with one process")
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()")
183
if num_processes > 1:
184
_log.info("Pre-forking %d server processes", num_processes)
185
for i in range(num_processes):
187
self.io_loop = ioloop.IOLoop.instance()
188
self.io_loop.add_handler(
189
self._socket.fileno(), self._handle_events,
195
self.io_loop = ioloop.IOLoop.instance()
196
self.io_loop.add_handler(self._socket.fileno(),
201
self.io_loop.remove_handler(self._socket.fileno())
204
def _handle_events(self, fd, events):
207
connection, address = self._socket.accept()
208
except socket.error, e:
209
if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
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)
217
stream = iostream.IOStream(connection, io_loop=self.io_loop)
218
HTTPConnection(stream, address, self.request_callback,
219
self.no_keep_alive, self.xheaders)
221
_log.error("Error in connection callback", exc_info=True)
224
class HTTPConnection(object):
225
"""Handles a connection to an HTTP client, executing HTTP requests.
227
We parse HTTP headers and bodies, and execute the request callback
228
until the HTTP conection is closed.
230
def __init__(self, stream, address, request_callback, no_keep_alive=False,
233
self.address = address
234
self.request_callback = request_callback
235
self.no_keep_alive = no_keep_alive
236
self.xheaders = xheaders
238
self._request_finished = False
239
self.stream.read_until("\r\n\r\n", self._on_headers)
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)
247
assert self._request, "Request closed"
248
self._request_finished = True
249
if not self.stream.writing():
250
self._finish_request()
252
def _on_write_complete(self):
253
if self._request_finished:
254
self._finish_request()
256
def _finish_request(self):
257
if self.no_keep_alive:
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"
269
self._request_finished = False
273
self.stream.read_until("\r\n\r\n", self._on_headers)
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])
286
content_length = headers.get("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)
296
self.request_callback(self._request)
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]
307
self._request.arguments.setdefault(name, []).extend(
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)
314
def _parse_mime_body(self, boundary, data):
315
if data.endswith("\r\n"):
316
footer_length = len(boundary) + 6
318
footer_length = len(boundary) + 4
319
parts = data[:-footer_length].split("--" + boundary + "\r\n")
321
if not part: continue
322
eoh = part.find("\r\n\r\n")
324
_log.warning("multipart/form-data missing headers")
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")
332
value = part[eoh + 4:-2]
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")
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,
347
self._request.arguments.setdefault(name, []).append(value)
350
class HTTPRequest(object):
351
"""A single HTTP request.
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.
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.
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.
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):
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"
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
390
scheme, netloc, path, query, fragment = urlparse.urlsplit(uri)
393
arguments = cgi.parse_qs(query)
395
for name, values in arguments.iteritems():
396
values = [v for v in values if v]
397
if values: self.arguments[name] = values
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"
403
def write(self, chunk):
404
"""Writes the given chunk to the response stream."""
405
assert isinstance(chunk, str)
406
self.connection.write(chunk)
409
"""Finishes this HTTP request on the open connection."""
410
self.connection.finish()
411
self._finish_time = time.time()
414
"""Reconstructs the full URL for this request."""
415
return self.protocol + "://" + self.host + self.uri
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
422
return self._finish_time - self._start_time
425
attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
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))
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)
437
def __getitem__(self, name):
438
return dict.__getitem__(self, self._normalize_name(name))
440
def _normalize_name(self, name):
441
return "-".join([w.capitalize() for w in name.split("-")])
444
def parse(cls, headers_string):
446
for line in headers_string.splitlines():
448
name, value = line.split(": ", 1)
449
headers[name] = value