Source code for automate.extensions.webui.webui

# -*- coding: utf-8 -*-
# (c) 2015 Tuomas Airaksinen
#
# This file is part of automate-webui.
#
# automate-webui is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# automate-webui is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with automate-webui.  If not, see <http://www.gnu.org/licenses/>.
#
# ------------------------------------------------------------------
#
# If you like Automate, please take a look at this page:
# http://evankelista.net/automate/

"""
    Web interface module for Automate
"""
from __future__ import unicode_literals

import json
import datetime
import os

from django.template.loader import render_to_string
import tornado.web
from tornado.websocket import WebSocketHandler, WebSocketClosedError

from traits.api import CBool, Tuple, Int, Str, CSet, List, CInt, Dict, Unicode

from automate.extensions.webui.djangoapp.views import set_globals
from automate.statusobject import StatusObject
from django.core.wsgi import get_wsgi_application
from automate.extensions.wsgi import TornadoService

[docs]class WebService(TornadoService): """ Web User Interface Service for Automate """ #: Restrict usage to only monitoring statuses (default: ``True``). #: If WebService is not in read_only mode, it is possible to run arbitrary Python commands #: through eval/exec via web browser. This is, of course, a severe security issue. #: Secure SSL configuration HIGHLY recommended, if not operating in ``read_only`` mode. read_only = CBool(True) #: Default view that is displayed when entering the server. Can be the name of any view in views.py default_view = Str('system') #: Below Actuator row, show active Programs that are controlling Actuator show_actuator_details = CBool(True) #: HTTP port to listen http_port = Int(8080) #: Authentication for logging into web server. (user,password) pairs in a tuple. http_auth = Tuple #: Let websocket connection die after ``websocket_timeout`` time of no ping reply from client. websocket_timeout = CInt(60 * 5) #: Tags that are shown in user defined view user_tags = CSet(trait=Str, value={'user'}) #: Django debugging mode (slower, more info shown when error occurs) debug = CBool(False) #: User-defined custom pages as a dictionary of form ``{name: template_content}`` custom_pages = Dict(key_trait=Unicode, value_trait=Unicode) #: set to True, if you want to launch multiple servers with same system. Authentication and #: other settings are then taken from master service. Only web server settings (http host/port) #: are used from slave. slave = CBool(False) #: In this dictionary you can define your custom Django settings which will override the default ones django_settings = Dict() # From /set/object/value and /toggle/object, redirect to /set_ready/object/value after after executing action redirect_from_setters = CBool(True) _sockets = List def get_filehandler_class(service): class MyFileHandler(tornado.web.StaticFileHandler): def validate_absolute_path(self, *args, **kwargs): session_id = getattr(self.request.cookies.get('sessionid', None), 'value', None) from django.contrib.sessions.middleware import SessionMiddleware mw = SessionMiddleware() session_data = mw.SessionStore(session_id) if not session_data.get('logged_in', False): raise tornado.web.HTTPError(403, 'not logged in') return super(MyFileHandler, self).validate_absolute_path(*args, **kwargs) def check_etag_header(self): """ Disable etag_header checking (checks only modified time). Due to etag caching file changes were not detected at all. """ return False return MyFileHandler def get_tornado_handlers(self): if self.slave: return self.system.request_service('WebService').get_tornado_handlers() super_handlers = super(WebService, self).get_tornado_handlers() path = os.path.join(os.path.dirname(__file__), 'static') static = [('/static/(.*)', tornado.web.StaticFileHandler, {'path': path})] return static + super_handlers def setup(self): if not self.slave: os.environ['DJANGO_SETTINGS_MODULE'] = 'automate.extensions.webui.settings' from django.conf import settings settings.DEBUG = self.debug if not 'SECRET_KEY' in self.django_settings: self.logger.warning('Insecure settings! Please set proper SECRET_KEY in django_settings!') for key, value in self.django_settings.items(): setattr(settings, key, value) set_globals(self, self.system) super(WebService, self).setup() if not self.slave: self.system.request_service('LogStoreService').on_trait_change(self.push_log, 'most_recent_line') self.system.on_trait_change(self.update_sockets, 'objects.status, objects.changing, objects.active, ' 'objects.program_status_items') def get_websocket(service): if service.slave: return service.system.request_service('WebService').get_websocket() class WebSocket(WebSocketHandler): def data_received(self, chunk): pass def __init__(self, application, request, **kwargs): self.log_requested = False self.subscribed_objects = set() self.last_message = None self.logged_in = False super(WebSocket, self).__init__(application, request, **kwargs) def check_origin(self, origin): return True def write_json(self, **kwargs): msg = json.dumps(kwargs) service.logger.debug('Sending to client %s', msg) self.write_message(msg) def open(self): self.session_id = session_id = getattr(self.request.cookies.get('sessionid', None), 'value', None) from django.contrib.sessions.middleware import SessionMiddleware mw = SessionMiddleware() session_data = mw.SessionStore(session_id) if session_data.get('logged_in', False) or not service.http_auth: self.logged_in = True else: service.logger.warning("Not (yet) logged in %s", session_id) service.logger.debug("WebSocket opened for session %s", session_id) service._sockets.append(self) def _authenticate(self, username, password): if (username, password) == service.http_auth: self.logged_in = True service.logger.debug('Websocket user %s logged in.', username) else: service.logger.warning('Authentication failure: user %s with passwd %s != %s ', username, password, service.http_auth) self.close() def _ping(self): pass def _set_status(self, name, status): if service.read_only: service.logger.warning("Could not perform operation: read only mode enabled") return obj = service.system.namespace.get(name, None) if obj: obj.status = status def _subscribe(self, objects): self.subscribed_objects.update(objects) def _unsubscribe(self, objects): self.subscribed_objects -= set(objects) def _clear_subscriptions(self): self.subscribed_objects.clear() def _send_command(self, command): if not service.read_only: service.system.cmd_exec(command) else: service.logger.warning("Could not perform operation: read only mode enabled") def _fetch_objects(self): data = [(i.name, i.get_as_datadict()) for i in service.system.objects_sorted] self.write_json(action='return', rv=data) def _request_log(self): self.log_requested = True def on_message(self, json_message): """ Message format: {'action': 'action_name', other_kwargs...) """ message = json.loads(json_message) service.logger.debug('Message received from client: %s', message) action = message.pop('action', '') if self.logged_in: action_func = getattr(self, '_' + action, None) if action_func: service.logger.debug('Running websocket action %s', action) action_func(**message) else: service.logger.error('Not logged in or unknown message %s', message) elif action == 'authenticate': return self._authenticate(**message) self.last_message = datetime.datetime.now() def on_close(self): service.logger.debug("WebSocket closed for session %s", self.session_id) service._sockets.remove(self) return WebSocket def push_log(self, new): for s in self._sockets: if s.log_requested: try: s.write_json(action='log', data=new) except WebSocketClosedError: pass def update_sockets(self, obj, attribute, old, new): if isinstance(obj, StatusObject): self.logger.debug('Update_sockets %s %s %s %s', obj, attribute, old, new) for s in self._sockets: if s.last_message and (s.last_message < datetime.datetime.now() - datetime.timedelta(seconds=self.websocket_timeout)): self.logger.info('Closing connection %s due to timeout', s.session_id) s.on_close() s.close(code=1000, reason='Timeout') continue if obj.name in s.subscribed_objects: if attribute == 'program_status_items' and self.show_actuator_details: new_actuator_html = render_to_string( 'rows/actuator_row.html', dict(actuator=obj, source='__SOURCE__', service=self)) s.write_json(action='update_actuator', name=obj.name, html=new_actuator_html) elif attribute == 'active': s.write_json(action='program_active', name=obj.name, active=obj.active) elif attribute in ['status', 'changing']: s.write_json(action='object_status', name=obj.name, status=obj.status, display=obj.get_status_display(), changing=obj.changing) def get_wsgi_application(self): return get_wsgi_application()