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)