From 9cf21aa03696ae89ebdeb3139f7eebe2a8941110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 7 Oct 2015 12:25:25 +0200 Subject: [PATCH] Also set caching headers for downloads to discourage client-side caching At least if necessary (added new boolean flag to the constructor of LargeResponseHandler for that). While at it also rename UrlForwardHandler to UrlProxyHandler (it does not forward, it proxies) and make it aware of ETag and Expires headers. --- src/octoprint/server/__init__.py | 24 ++++++++++++++++++++---- src/octoprint/server/util/tornado.py | 15 +++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 646e573d..a6a6aa24 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -330,13 +330,29 @@ class Server(): upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"])) + download_handler_kwargs = dict( + as_attachment=True, + allow_client_caching=False + ) + admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator)) + no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404)) + + def joined_dict(*dicts): + if not len(dicts): + return dict() + + joined = dict() + for d in dicts: + joined.update(d) + return joined + server_routes = self._router.urls + [ # various downloads - (r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)), - (r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))), - (r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))), + (r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("timelapse")), download_handler_kwargs, no_hidden_files_validator)), + (r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("uploads")), download_handler_kwargs, no_hidden_files_validator)), + (r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("logs")), download_handler_kwargs, admin_validator)), # camera snapshot - (r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), + (r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), # generated webassets (r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets"))) ] diff --git a/src/octoprint/server/util/tornado.py b/src/octoprint/server/util/tornado.py index 14ace0da..d69f3036 100644 --- a/src/octoprint/server/util/tornado.py +++ b/src/octoprint/server/util/tornado.py @@ -768,6 +768,8 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): :class:``~tornado.web.StaticFileHandler`` as the ``default_filename`` keyword parameter). Defaults to ``None``. as_attachment (bool): Whether to serve requested files with ``Content-Disposition: attachment`` header (``True``) or not. Defaults to ``False``. + allow_client_caching (bool): Whether to allow the client to cache (by not setting any ``Cache-Control`` or + ``Expires`` headers on the response) or not. access_validation (function): Callback to call in the ``get`` method to validate access to the resource. Will be called with ``self.request`` as parameter which contains the full tornado request object. Should raise a ``tornado.web.HTTPError`` if access is not allowed in which case the request will not be further processed. @@ -782,10 +784,11 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): by ``get_content_version``. """ - def initialize(self, path, default_filename=None, as_attachment=False, + def initialize(self, path, default_filename=None, as_attachment=False, allow_client_caching=True, access_validation=None, path_validation=None, etag_generator=None): tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename) self._as_attachment = as_attachment + self._allow_client_caching = allow_client_caching self._access_validation = access_validation self._path_validation = path_validation self._etag_generator = etag_generator @@ -802,6 +805,10 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): if self._as_attachment: self.set_header("Content-Disposition", "attachment") + if not self._allow_client_caching: + self.set_header("Cache-Control", "max-age=0, must-revalidate, private") + self.set_header("Expires", "-1") + def compute_etag(self): if self._etag_generator is not None: return self._etag_generator(self) @@ -817,7 +824,7 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): ##~~ URL Forward Handler for forwarding requests to a preconfigured static URL -class UrlForwardHandler(tornado.web.RequestHandler): +class UrlProxyHandler(tornado.web.RequestHandler): """ `tornado.web.RequestHandler `_ that proxies requests to a preconfigured url and returns the response. Allows delivery of the requested content as attachment @@ -827,7 +834,7 @@ class UrlForwardHandler(tornado.web.RequestHandler): for making the request to the configured endpoint and return the body of the client response with the status code from the client response and the following headers: - * ``Date``, ``Cache-Control``, ``Server``, ``Content-Type`` and ``Location`` will be copied over. + * ``Date``, ``Cache-Control``, ``Expires``, ``ETag``, ``Server``, ``Content-Type`` and ``Location`` will be copied over. * If ``as_attachment`` is set to True, ``Content-Disposition`` will be set to ``attachment``. If ``basename`` is set including the attachement's ``filename`` attribute will be set to the base name followed by the extension guessed based on the MIME type from the ``Content-Type`` header of the response. If no extension can be guessed @@ -877,7 +884,7 @@ class UrlForwardHandler(tornado.web.RequestHandler): filename = None self.set_status(response.code) - for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location"): + for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location", "Expires", "ETag"): value = response.headers.get(name) if value: self.set_header(name, value)