Static endpoint in tornado for fetching the current webcam snaphot (authenticated users/apikey only)

This commit is contained in:
Gina Häußge 2014-05-14 22:22:04 +02:00
parent e7088ef06c
commit da19ad7943
2 changed files with 117 additions and 12 deletions

View file

@ -32,7 +32,8 @@ admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user"))
# only import the octoprint stuff down here, as it might depend on things defined above to be initialized already
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator, \
UrlForwardHandler, user_validator
from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings
import octoprint.gcodefiles as gcodefiles
@ -182,23 +183,32 @@ class Server():
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
def admin_access_validation(request):
def access_validation_factory(validator):
"""
Creates a custom wsgi and Flask request context in order to be able to process user information
stored in the current session.
Creates an access validation wrapper using the supplied validator.
:param request: The Tornado request for which to create the environment and context
: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
"""
wsgi_environ = tornado.wsgi.WSGIContainer.environ(request)
with app.request_context(wsgi_environ):
app.session_interface.open_session(app, flask.request)
loginManager.reload_user()
admin_validator(flask.request)
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
"""
wsgi_environ = tornado.wsgi.WSGIContainer.environ(request)
with app.request_context(wsgi_environ):
app.session_interface.open_session(app, flask.request)
loginManager.reload_user()
validator(flask.request)
return f
self._tornado_app = Application(self._router.urls + [
(r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
(r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
(r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True, "access_validation": admin_access_validation}),
(r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True, "access_validation": access_validation_factory(admin_validator)}),
(r"/downloads/camera/current", UrlForwardHandler, {"url": settings().get(["webcam", "snapshot"]), "as_attachment": True, "access_validation": access_validation_factory(user_validator)}),
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
])
self._server = HTTPServer(self._tornado_app)

View file

@ -3,7 +3,8 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from flask.ext.principal import identity_changed, Identity
from tornado.web import StaticFileHandler, HTTPError
from tornado.web import StaticFileHandler, HTTPError, RequestHandler, asynchronous
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from flask import url_for, make_response, request, current_app
from flask.ext.login import login_required, login_user, current_user
from werkzeug.utils import redirect
@ -307,6 +308,77 @@ class LargeResponseHandler(StaticFileHandler):
self.set_header("Content-Disposition", "attachment")
##~~ URL Forward Handler for forwarding requests to a preconfigured static URL
class UrlForwardHandler(RequestHandler):
def initialize(self, url=None, as_attachment=False, basename=None, access_validation=None):
RequestHandler.initialize(self)
self._url = url
self._as_attachment = as_attachment
self._basename = basename
self._access_validation = access_validation
@asynchronous
def get(self, *args, **kwargs):
if self._access_validation is not None:
self._access_validation(self.request)
if self._url is None:
raise HTTPError(404)
client = AsyncHTTPClient()
r = 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 HTTPError as e:
if hasattr(e, "response") and e.response:
self.handle_response(e.response)
else:
raise HTTPError(500)
def handle_response(self, response):
if response.error and not isinstance(response.error, HTTPError):
raise 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)
#~~ admin access validator for use with tornado
@ -330,6 +402,29 @@ def admin_validator(request):
raise HTTPError(403)
#~~ user access validator for use with tornado
def user_validator(request):
"""
Validates that the given request is made by an authenticated user, identified either by API key or existing Flask
session.
Must be executed in an existing Flask request context!
:param request: The Flask request object
"""
apikey = getApiKey(request)
if settings().get(["api", "enabled"]) and apikey is not None:
user = getUserForApiKey(apikey)
else:
user = current_user
if user is None or not user.is_authenticated():
raise HTTPError(403)
#~~ reverse proxy compatible wsgi middleware