Static endpoint in tornado for fetching the current webcam snaphot (authenticated users/apikey only)
This commit is contained in:
parent
e7088ef06c
commit
da19ad7943
2 changed files with 117 additions and 12 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue