MrDraw/src/octoprint/server/util/tornado.py

880 lines
30 KiB
Python

# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
import logging
import os
import datetime
import stat
import mimetypes
import email
import time
import re
import tornado
import tornado.web
import tornado.gen
import tornado.escape
import tornado.httputil
import tornado.httpserver
import tornado.httpclient
import tornado.http1connection
import tornado.iostream
import tornado.tcpserver
import octoprint.util
#~~ WSGI middleware
@tornado.web.stream_request_body
class UploadStorageFallbackHandler(tornado.web.RequestHandler):
"""
A `RequestHandler` similar to `tornado.web.FallbackHandler` which fetches any files contained in the request bodies
of content type `multipart`, stores them in temporary files and supplies the `fallback` with the file's `name`,
`content_type`, `path` and `size` instead via a rewritten body.
Basically similar to what the nginx upload module does.
Basic request body example:
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file"; filename="test.gcode"
Content-Type: application/octet-stream
...
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="apikey"
my_funny_apikey
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="select"
true
------WebKitFormBoundarypYiSUx63abAmhT5C--
That would get turned into:
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="apikey"
my_funny_apikey
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="select"
true
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.path"
/tmp/tmpzupkro
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.name"
test.gcode
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.content_type"
application/octet-stream
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.size"
349182
------WebKitFormBoundarypYiSUx63abAmhT5C--
The underlying application can then access the contained files via their respective paths and just move them
where necessary.
"""
# the request methods that may contain a request body
BODY_METHODS = ("POST", "PATCH", "PUT")
def initialize(self, fallback, file_prefix="tmp", file_suffix="", path=None, suffixes=None):
if not suffixes:
suffixes = dict()
self._fallback = fallback
self._file_prefix = file_prefix
self._file_suffix = file_suffix
self._path = path
self._suffixes = dict((key, key) for key in ("name", "path", "content_type", "size"))
for suffix_type, suffix in suffixes.iteritems():
if suffix_type in self._suffixes and suffix is not None:
self._suffixes[suffix_type] = suffix
# Parts, files and values will be stored here
self._parts = dict()
self._files = []
# Part currently being processed
self._current_part = None
# content type of request body
self._content_type = None
# bytes left to read according to content_length of request body
self._bytes_left = 0
# buffer needed for identifying form data parts
self._buffer = b""
# buffer for new body
self._new_body = b""
# logger
self._logger = logging.getLogger(__name__)
def prepare(self):
"""
Prepares the processing of the request. If it's a request that may contain a request body (as defined in
`UploadStorageFallbackHandler.BODY_METHODS) prepares the multipart parsing if content type fits. If it's a
body-less request, just calls the `fallback` with an empty body and finishes the request.
"""
if self.request.method in UploadStorageFallbackHandler.BODY_METHODS:
self._bytes_left = self.request.headers.get("Content-Length", 0)
self._content_type = self.request.headers.get("Content-Type", None)
# request might contain a body
if self.is_multipart():
if not self._bytes_left:
# we don't support requests without a content-length
raise tornado.web.HTTPError(400, reason="No Content-Length supplied")
# extract the multipart boundary
fields = self._content_type.split(";")
for field in fields:
k, sep, v = field.strip().partition("=")
if k == "boundary" and v:
if v.startswith(b'"') and v.endswith(b'"'):
self._multipart_boundary = tornado.escape.utf8(v[1:-1])
else:
self._multipart_boundary = tornado.escape.utf8(v)
break
else:
self._multipart_boundary = None
else:
self._fallback(self.request, b"")
self._finished = True
def data_received(self, chunk):
"""
Called by Tornado on receiving a chunk of the request body. If request is a multipart request, takes care of
processing the multipart data structure via `self._process_multipart_data`. If not, just adds the chunk to
internal in-memory buffer.
:param chunk: chunk of data received from Tornado
"""
data = self._buffer + chunk
if self.is_multipart():
self._process_multipart_data(data)
else:
self._buffer = data
def is_multipart(self):
"""Checks whether this request is a `multipart` request"""
return self._content_type is not None and self._content_type.startswith("multipart")
def _process_multipart_data(self, data):
"""
Processes the given data, parsing it for multipart definitions and calling the appropriate methods.
:param data: the data to process as a string
"""
# check for boundary
delimiter = b"--%s" % self._multipart_boundary
delimiter_loc = data.find(delimiter)
delimiter_len = len(delimiter)
end_of_header = None
if delimiter_loc != -1:
# found the delimiter in the currently available data
data, self._buffer = data[0:delimiter_loc], data[delimiter_loc:]
end_of_header = self._buffer.find("\r\n\r\n")
else:
# make sure any boundary (with single or double ==) contained at the end of chunk does not get
# truncated by this processing round => save it to the buffer for next round
endlen = len(self._multipart_boundary) + 4
data, self._buffer = data[0:-endlen], data[-endlen:]
# stream data to part handler
if data and self._current_part:
self._on_part_data(self._current_part, data)
if end_of_header >= 0:
self._on_part_header(self._buffer[delimiter_len+2:end_of_header])
self._buffer = self._buffer[end_of_header + 4:]
if delimiter_loc != -1 and self._buffer[delimiter_len:delimiter_len+2] == "--":
# we saw the last boundary and are at the end of our request
if self._current_part:
self._on_part_finish(self._current_part)
self._current_part = None
self._buffer = b""
self._on_request_body_finish()
def _on_part_header(self, header):
"""
Called for a new multipart header, takes care of parsing the header and calling `self._on_part` with the
relevant data, setting the current part in the process.
:param header: header to parse
"""
# close any open parts
if self._current_part:
self._on_part_finish(self._current_part)
self._current_part = None
header_check = header.find(self._multipart_boundary)
if header_check != -1:
self._logger.warn("Header still contained multipart boundary, stripping it...")
header = header[header_check:]
# convert to dict
header = tornado.httputil.HTTPHeaders.parse(header.decode("utf-8"))
disp_header = header.get("Content-Disposition", "")
disposition, disp_params = tornado.httputil._parse_header(disp_header)
if disposition != "form-data":
self._logger.warn("Got a multipart header without form-data content disposition, ignoring that one")
return
if not disp_params.get("name"):
self._logger.warn("Got a multipart header without name, ignoring that one")
return
self._current_part = self._on_part_start(disp_params["name"], header.get("Content-Type", None), filename=disp_params["filename"] if "filename" in disp_params else None)
def _on_part_start(self, name, content_type, filename=None):
"""
Called for new parts in the multipart stream. If `filename` is given creates new `file` part (which leads
to storage of the data as temporary file on disk), if not creates a new `data` part (which stores
incoming data in memory).
Structure of `file` parts:
* `name`: name of the part
* `filename`: filename associated with the part
* `path`: path to the temporary file storing the file's data
* `content_type`: content type of the part
* `file`: file handle for the temporary file (mode "wb", not deleted on close!)
Structure of `data` parts:
* `name`: name of the part
* `content_type`: content type of the part
* `data`: bytes of the part (initialized to "")
:param name: name of the part
:param content_type: content type of the part
:param filename: filename associated with the part.
:return: dict describing the new part
"""
if filename is not None:
# this is a file
import tempfile
handle = tempfile.NamedTemporaryFile(mode="wb", prefix=self._file_prefix, suffix=self._file_suffix, dir=self._path, delete=False)
return dict(name=tornado.escape.utf8(name),
filename=tornado.escape.utf8(filename),
path=tornado.escape.utf8(handle.name),
content_type=tornado.escape.utf8(content_type),
file=handle)
else:
return dict(name=tornado.escape.utf8(name), content_type=content_type, data=b"")
def _on_part_data(self, part, data):
"""
Called when new bytes are received for the given `part`, takes care of writing them to their storage.
:param part: part for which data was received
:param data: data chunk which was received
"""
if "file" in part:
part["file"].write(data)
else:
part["data"] += data
def _on_part_finish(self, part):
"""
Called when a part gets closed, takes care of storing the finished part in the internal parts storage and for
`file` parts closing the temporary file and storing the part in the internal files storage.
:param part: part which was closed
"""
name = part["name"]
self._parts[name] = part
if "file" in part:
self._files.append(part["path"])
part["file"].close()
del part["file"]
def _on_request_body_finish(self):
"""
Called when the request body has been read completely. Takes care of creating the replacement body out of the
logged parts, turning `file` parts into new
:return:
"""
self._new_body = b""
for name, part in self._parts.iteritems():
if "filename" in part:
# add form fields for filename, path, size and content_type for all files contained in the request
fields = dict((self._suffixes[key], value) for (key, value) in dict(name=part["filename"], path=part["path"], size=str(os.stat(part["path"]).st_size), content_type=part["content_type"]).iteritems())
for n, p in fields.iteritems():
key = name + "." + n
self._new_body += b"--%s\r\n" % self._multipart_boundary
self._new_body += b"Content-Disposition: form-data; name=\"%s\"\r\n" % key
self._new_body += b"\r\n"
self._new_body += p + b"\r\n"
elif "data" in part:
self._new_body += b"--%s\r\n" % self._multipart_boundary
value = part["data"]
self._new_body += b"Content-Disposition: form-data; name=\"%s\"\r\n" % name
if "content_type" in part and part["content_type"] is not None:
self._new_body += b"Content-Type: %s\r\n" % part["content_type"]
self._new_body += b"\r\n"
self._new_body += value
self._new_body += b"--%s--\r\n" % self._multipart_boundary
def _handle_method(self, *args, **kwargs):
"""
Handler for any request method, takes care of defining the new request body if necessary and forwarding
the current request and changed body to the `fallback`.
"""
# determine which body to supply
body = b""
if self.is_multipart():
# make sure we really processed all data in the buffer
while len(self._buffer):
self._process_multipart_data(self._buffer)
# use rewritten body
body = self._new_body
elif self.request.method in UploadStorageFallbackHandler.BODY_METHODS:
# directly use data from buffer
body = self._buffer
# rewrite content length
self.request.headers["Content-Length"] = len(body)
try:
# call the configured fallback with request and body to use
self._fallback(self.request, body)
self._headers_written = True
finally:
# make sure the temporary files are removed again
for f in self._files:
octoprint.util.silentRemove(f)
# make all http methods trigger _handle_method
get = _handle_method
post = _handle_method
put = _handle_method
patch = _handle_method
delete = _handle_method
head = _handle_method
options = _handle_method
class WsgiInputContainer(object):
"""
A WSGI container for use with Tornado that allows supplying the request body to be used for `wsgi.input` in the
generated WSGI environment upon call.
A `RequestHandler` can thus provide the WSGI application with a stream for the request body, or a modified body.
Example usage:
wsgi_app = octoprint.server.util.WsgiInputContainer(octoprint_app)
application = tornado.web.Application([
(r".*", UploadStorageFallbackHandler, dict(fallback=wsgi_app),
])
The implementation logic is basically the same as `tornado.wsgi.WSGIContainer` but the `__call__` and `environ`
methods have been adjusted to for an optionally supplied `body` argument which is then used for `wsgi.input`.
"""
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application
def __call__(self, request, body=None):
"""
Wraps the call against the WSGI app, deriving the WSGI environment from the supplied Tornado `HTTPServerRequest`.
:param request: the `tornado.httpserver.HTTPServerRequest` to derive the WSGI environment from
:param body: an optional body to use as `wsgi.input` instead of `request.body`, can be a string or a stream
"""
data = {}
response = []
def start_response(status, response_headers, exc_info=None):
data["status"] = status
data["headers"] = response_headers
return response.append
app_response = self.wsgi_application(
WsgiInputContainer.environ(request, body), start_response)
try:
response.extend(app_response)
body = b"".join(response)
finally:
if hasattr(app_response, "close"):
app_response.close()
if not data:
raise Exception("WSGI app did not call start_response")
status_code = int(data["status"].split()[0])
headers = data["headers"]
header_set = set(k.lower() for (k, v) in headers)
body = tornado.escape.utf8(body)
if status_code != 304:
if "content-length" not in header_set:
headers.append(("Content-Length", str(len(body))))
if "content-type" not in header_set:
headers.append(("Content-Type", "text/html; charset=UTF-8"))
if "server" not in header_set:
headers.append(("Server", "TornadoServer/%s" % tornado.version))
parts = [tornado.escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
for key, value in headers:
parts.append(tornado.escape.utf8(key) + b": " + tornado.escape.utf8(value) + b"\r\n")
parts.append(b"\r\n")
parts.append(body)
request.write(b"".join(parts))
request.finish()
self._log(status_code, request)
@staticmethod
def environ(request, body=None):
"""
Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
An optional `body` to be used for populating `wsgi.input` can be supplied (either a string or a stream). If not
supplied, `request.body` will be wrapped into a `io.BytesIO` stream and used instead.
:param request: the `tornado.httpserver.HTTPServerRequest` to derive the WSGI environment from
:param body: an optional body to use as `wsgi.input` instead of `request.body`, can be a string or a stream
"""
from tornado.wsgi import to_wsgi_str
import sys
import io
# determine the request_body to supply as wsgi.input
if body is not None:
if isinstance(body, (bytes, str)):
request_body = io.BytesIO(tornado.escape.utf8(body))
else:
request_body = body
else:
request_body = io.BytesIO(tornado.escape.utf8(request.body))
hostport = request.host.split(":")
if len(hostport) == 2:
host = hostport[0]
port = int(hostport[1])
else:
host = request.host
port = 443 if request.protocol == "https" else 80
environ = {
"REQUEST_METHOD": request.method,
"SCRIPT_NAME": "",
"PATH_INFO": to_wsgi_str(tornado.escape.url_unescape(
request.path, encoding=None, plus=False)),
"QUERY_STRING": request.query,
"REMOTE_ADDR": request.remote_ip,
"SERVER_NAME": host,
"SERVER_PORT": str(port),
"SERVER_PROTOCOL": request.version,
"wsgi.version": (1, 0),
"wsgi.url_scheme": request.protocol,
"wsgi.input": request_body,
"wsgi.errors": sys.stderr,
"wsgi.multithread": False,
"wsgi.multiprocess": True,
"wsgi.run_once": False,
}
if "Content-Type" in request.headers:
environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
if "Content-Length" in request.headers:
environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
for key, value in request.headers.items():
environ["HTTP_" + key.replace("-", "_").upper()] = value
return environ
def _log(self, status_code, request):
access_log = logging.getLogger("tornado.access")
if status_code < 400:
log_method = access_log.info
elif status_code < 500:
log_method = access_log.warning
else:
log_method = access_log.error
request_time = 1000.0 * request.request_time()
summary = request.method + " " + request.uri + " (" + \
request.remote_ip + ")"
log_method("%d %s %.2fms", status_code, summary, request_time)
#~~ customized HTTP1Connection implementation
class CustomHTTPServer(tornado.httpserver.HTTPServer):
"""
Custom implementation of `tornado.httpserver.HTTPServer` that allows defining max body sizes depending on path and
method.
The implementation is mostly taken from `tornado.httpserver.HTTPServer`, the only difference is the creation
of a `CustomHTTP1ConnectionParameters` instance instead of `tornado.http1connection.HTTP1ConnectionParameters`
which is supplied with the two new constructor arguments `max_body_sizes` and `max_default_body_size` and the
creation of a `CustomHTTP1ServerConnection` instead of a `tornado.http1connection.HTTP1ServerConnection` upon
connection by a client.
`max_body_sizes` is expected to be an iterable containing tuples of the form (method, path regex, maximum body size),
with method and path regex having to match in order for maximum body size to take affect.
`default_max_body_size` is the default maximum body size to apply if no specific one from `max_body_sizes` matches.
"""
def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
xheaders=False, ssl_options=None, protocol=None,
decompress_request=False,
chunk_size=None, max_header_size=None,
idle_connection_timeout=None, body_timeout=None,
max_body_sizes=None, default_max_body_size=None, max_buffer_size=None):
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self.protocol = protocol
self.conn_params = CustomHTTP1ConnectionParameters(
decompress=decompress_request,
chunk_size=chunk_size,
max_header_size=max_header_size,
header_timeout=idle_connection_timeout or 3600,
max_body_sizes=max_body_sizes,
default_max_body_size=default_max_body_size,
body_timeout=body_timeout)
tornado.tcpserver.TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
max_buffer_size=max_buffer_size,
read_chunk_size=chunk_size)
self._connections = set()
def handle_stream(self, stream, address):
context = tornado.httpserver._HTTPRequestContext(stream, address,
self.protocol)
conn = CustomHTTP1ServerConnection(
stream, self.conn_params, context)
self._connections.add(conn)
conn.start_serving(self)
class CustomHTTP1ServerConnection(tornado.http1connection.HTTP1ServerConnection):
"""
A custom implementation of `tornado.http1connection.HTTP1ServerConnection` which utilizes a `CustomHTTP1Connection`
instead of a `tornado.http1connection.HTTP1Connection` in `_server_request_loop`. The implementation logic is
otherwise the same as `tornado.http1connection.HTTP1ServerConnection`.
"""
@tornado.gen.coroutine
def _server_request_loop(self, delegate):
try:
while True:
conn = CustomHTTP1Connection(self.stream, False,
self.params, self.context)
request_delegate = delegate.start_request(self, conn)
try:
ret = yield conn.read_response(request_delegate)
except (tornado.iostream.StreamClosedError,
tornado.iostream.UnsatisfiableReadError):
return
except tornado.http1connection._QuietException:
# This exception was already logged.
conn.close()
return
except Exception:
tornado.http1connection.gen_log.error("Uncaught exception", exc_info=True)
conn.close()
return
if not ret:
return
yield tornado.gen.moment
finally:
delegate.on_close(self)
class CustomHTTP1Connection(tornado.http1connection.HTTP1Connection):
"""
A custom implementation of `tornado.http1connection.HTTP1Connection` which upon checking the `Content-Length` of
the request against the configured maximum utilizes `max_body_sizes` and `default_max_body_size` as a fallback.
"""
def __init__(self, stream, is_client, params=None, context=None):
tornado.http1connection.HTTP1Connection.__init__(self, stream, is_client, params=params, context=context)
import re
self._max_body_sizes = map(lambda x: (x[0], re.compile(x[1]), x[2]), self.params.max_body_sizes or dict())
self._default_max_body_size = self.params.default_max_body_size or self.stream.max_buffer_size
def _read_body(self, code, headers, delegate):
"""
Basically the same as `tornado.http1connection.HTTP1Connection._read_body`, but determines the maximum
content length individually for the request (utilizing `._get_max_content_length`).
If the individual max content length is 0 or smaller no content length is checked. If the content length of the
current request exceeds the individual max content length, the request processing is aborted and an
`HTTPInputError` is raised.
"""
content_length = headers.get("Content-Length")
if "Content-Length" in headers:
if "," in headers["Content-Length"]:
# Proxies sometimes cause Content-Length headers to get
# duplicated. If all the values are identical then we can
# use them but if they differ it's an error.
pieces = re.split(r',\s*', headers["Content-Length"])
if any(i != pieces[0] for i in pieces):
raise tornado.httputil.HTTPInputError(
"Multiple unequal Content-Lengths: %r" %
headers["Content-Length"])
headers["Content-Length"] = pieces[0]
content_length = int(headers["Content-Length"])
content_length = int(content_length)
max_content_length = self._get_max_content_length(self._request_start_line.method, self._request_start_line.path)
if 0 <= max_content_length < content_length:
raise tornado.httputil.HTTPInputError("Content-Length too long")
else:
content_length = None
if code == 204:
# This response code is not allowed to have a non-empty body,
# and has an implicit length of zero instead of read-until-close.
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
if ("Transfer-Encoding" in headers or
content_length not in (None, 0)):
raise tornado.httputil.HTTPInputError(
"Response with code %d should not have body" % code)
content_length = 0
if content_length is not None:
return self._read_fixed_body(content_length, delegate)
if headers.get("Transfer-Encoding") == "chunked":
return self._read_chunked_body(delegate)
if self.is_client:
return self._read_body_until_close(delegate)
return None
def _get_max_content_length(self, method, path):
"""
Gets the max content length for the given method and path. Checks whether method and path match against any
of the specific maximum content lengths supplied in `max_body_sizes` and returns that as the maximum content
length if available, otherwise returns `default_max_body_size`.
:param method: method of the request to match against
:param path: path od the request to match against
:return: determine maximum content length to apply to this request, max return 0 for unlimited allowed content
length
"""
for m, p, s in self._max_body_sizes:
if method == m and p.match(path):
return s
return self._default_max_body_size
class CustomHTTP1ConnectionParameters(tornado.http1connection.HTTP1ConnectionParameters):
"""
An implementation of `tornado.http1connection.HTTP1ConnectionParameters` that adds to new parameters
`max_body_sizes` and `default_max_body_size`.
For a description of these please see the documentation of `CustomHTTPServer` above.
"""
def __init__(self, *args, **kwargs):
tornado.http1connection.HTTP1ConnectionParameters.__init__(self, args, kwargs)
self.max_body_sizes = kwargs["max_body_sizes"] if "max_body_sizes" in kwargs else dict()
self.default_max_body_size = kwargs["default_max_body_size"] if "default_max_body_size" in kwargs else dict()
#~~ customized large response handler
class LargeResponseHandler(tornado.web.StaticFileHandler):
CHUNK_SIZE = 16 * 1024
def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None):
tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename)
self._as_attachment = as_attachment
self._access_validation = access_validation
def get(self, path, include_body=True):
if self._access_validation is not None:
self._access_validation(self.request)
path = self.parse_url_path(path)
abspath = os.path.abspath(os.path.join(self.root, path))
# os.path.abspath strips a trailing /
# it needs to be temporarily added back for requests to root/
if not (abspath + os.path.sep).startswith(self.root):
raise tornado.web.HTTPError(403, "%s is not in root static directory", path)
if os.path.isdir(abspath) and self.default_filename is not None:
# need to look at the request.path here for when path is empty
# but there is some prefix to the path that was already
# trimmed by the routing
if not self.request.path.endswith("/"):
self.redirect(self.request.path + "/")
return
abspath = os.path.join(abspath, self.default_filename)
if not os.path.exists(abspath):
raise tornado.web.HTTPError(404)
if not os.path.isfile(abspath):
raise tornado.web.HTTPError(403, "%s is not a file", path)
stat_result = os.stat(abspath)
modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
self.set_header("Last-Modified", modified)
mime_type, encoding = mimetypes.guess_type(abspath)
if mime_type:
self.set_header("Content-Type", mime_type)
cache_time = self.get_cache_time(path, modified, mime_type)
if cache_time > 0:
self.set_header("Expires", datetime.datetime.utcnow() +
datetime.timedelta(seconds=cache_time))
self.set_header("Cache-Control", "max-age=" + str(cache_time))
self.set_extra_headers(path)
# Check the If-Modified-Since, and don't send the result if the
# content has not been modified
ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value)
if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
if if_since >= modified:
self.set_status(304)
return
if not include_body:
assert self.request.method == "HEAD"
self.set_header("Content-Length", stat_result[stat.ST_SIZE])
else:
with open(abspath, "rb") as file:
while True:
data = file.read(LargeResponseHandler.CHUNK_SIZE)
if not data:
break
self.write(data)
self.flush()
def set_extra_headers(self, path):
if self._as_attachment:
self.set_header("Content-Disposition", "attachment")
##~~ URL Forward Handler for forwarding requests to a preconfigured static URL
class UrlForwardHandler(tornado.web.RequestHandler):
def initialize(self, url=None, as_attachment=False, basename=None, access_validation=None):
tornado.web.RequestHandler.initialize(self)
self._url = url
self._as_attachment = as_attachment
self._basename = basename
self._access_validation = access_validation
@tornado.web.asynchronous
def get(self, *args, **kwargs):
if self._access_validation is not None:
self._access_validation(self.request)
if self._url is None:
raise tornado.web.HTTPError(404)
client = tornado.httpclient.AsyncHTTPClient()
r = tornado.httpclient.HTTPRequest(url=self._url, method=self.request.method, body=self.request.body, headers=self.request.headers, follow_redirects=False, allow_nonstandard_methods=True)
try:
return client.fetch(r, self.handle_response)
except tornado.web.HTTPError as e:
if hasattr(e, "response") and e.response:
self.handle_response(e.response)
else:
raise tornado.web.HTTPError(500)
def handle_response(self, response):
if response.error and not isinstance(response.error, tornado.web.HTTPError):
raise tornado.web.HTTPError(500)
filename = None
self.set_status(response.code)
for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location"):
value = response.headers.get(name)
if value:
self.set_header(name, value)
if name == "Content-Type":
filename = self.get_filename(value)
if self._as_attachment:
if filename is not None:
self.set_header("Content-Disposition", "attachment; filename=%s" % filename)
else:
self.set_header("Content-Disposition", "attachment")
if response.body:
self.write(response.body)
self.finish()
def get_filename(self, content_type):
if not self._basename:
return None
typeValue = map(str.strip, content_type.split(";"))
if len(typeValue) == 0:
return None
extension = mimetypes.guess_extension(typeValue[0])
if not extension:
return None
return "%s%s" % (self._basename, extension)
#~~ Factory method for creating Flask access validation wrappers from the Tornado request context
def access_validation_factory(app, login_manager, validator):
"""
Creates an access validation wrapper using the supplied validator.
:param validator: the access validator to use inside the validation wrapper
:return: an access validation wrapper taking a request as parameter and performing the request validation
"""
def f(request):
"""
Creates a custom wsgi and Flask request context in order to be able to process user information
stored in the current session.
:param request: The Tornado request for which to create the environment and context
"""
import flask
wsgi_environ = WsgiInputContainer.environ(request)
with app.request_context(wsgi_environ):
app.session_interface.open_session(app, flask.request)
login_manager.reload_user()
validator(flask.request)
return f