class WebInterface(YomboLibrary): """ Web interface framework. """ webapp = Klein() # Like Flask, but for twisted visits = 0 alerts = OrderedDict() def _init_(self): self.enabled = self._Configs.get('webinterface', 'enabled', True) if not self.enabled: return self.gwid = self._Configs.get("core", "gwid") self._LocalDb = self._Loader.loadedLibraries['localdb'] self._current_dir = self._Atoms.get('yombo.path') + "/yombo" print "web interface direct1: %s" % self._current_dir self._dir = '/lib/webinterface/' self._build_dist() # Make all the JS and CSS files self.api = self._Loader.loadedLibraries['yomboapi'] self._VoiceCmds = self._Loader.loadedLibraries['voicecmds'] self.misc_wi_data = {} self.sessions = Sessions(self._Loader) self.wi_port_nonsecure = self._Configs.get('webinterface', 'nonsecure_port', 8080) self.wi_port_secure = self._Configs.get('webinterface', 'secure_port', 8443) self.webapp.templates = jinja2.Environment(loader=jinja2.FileSystemLoader(self._current_dir)) self.setup_basic_filters() route_atoms(self.webapp) route_automation(self.webapp) route_api_v1(self.webapp) route_commands(self.webapp) route_configs(self.webapp) route_devices(self.webapp) route_devtools(self.webapp) route_modules(self.webapp) route_notices(self.webapp) route_setup_wizard(self.webapp) route_statistics(self.webapp) route_states(self.webapp) route_system(self.webapp) route_voicecmds(self.webapp) self.temp_data = ExpiringDict(max_age_seconds=1800) @webapp.handle_errors(NotFound) @require_auth() def notfound(self, request, failure): request.setResponseCode(404) return 'Not found, I say' @webapp.route('/<path:catchall>') @run_first() @require_auth() def page_404(self, request, session, catchall): request.setResponseCode(404) page = self.get_template(request, self._dir + 'pages/404.html') return page.render() @inlineCallbacks def _load_(self): yield self.sessions.init() def _start_(self): if not self.enabled: return self._op_mode = self._Atoms['loader.operation_mode'] self.auth_pin = self._Configs.get('webinterface', 'auth_pin', yombo.utils.random_string(length=4, letters=yombo.utils.human_alpabet()).lower()) self.auth_pin_totp = self._Configs.get('webinterface', 'auth_pin_totp', yombo.utils.random_string(length=16)) self.auth_pin_type = self._Configs.get('webinterface', 'auth_pin_type', 'pin') self.auth_pin_required = self._Configs.get('webinterface', 'auth_pin_required', True) self.web_factory = Site(self.webapp.resource(), None, logPath='/dev/null') self.web_factory.noisy = False # turn off Starting/stopping message # self.web_factory.sessionFactory = YomboSession self.displayTracebacks = False self.web_interface_listener = reactor.listenTCP(self.wi_port_nonsecure, self.web_factory) self._display_pin_console_time = 0 self.misc_wi_data['gateway_configured'] = self._home_gateway_configured() self.misc_wi_data['gateway_label'] = self._Configs.get('core', 'label', 'Yombo Gateway', False) self.misc_wi_data['operation_mode'] = self._op_mode self.misc_wi_data['notifications'] = self._Notifications self.misc_wi_data['notification_priority_map_css'] = notification_priority_map_css self.misc_wi_data['breadcrumb'] = [] # self.functions = { # 'yes_no': yombo.utils.is_yes_no, # } self.webapp.templates.globals['misc_wi_data'] = self.misc_wi_data # self.webapp.templates.globals['func'] = self.functions def _module_started_(self, **kwargs): """ to be deprecated. use i18n below. :param kwargs: :return: """ self.webapp.templates.globals['_'] = _ # i18n def _started_(self): # if self._op_mode != 'run': self._display_pin_console_time = int(time()) self.display_pin_console() def _unload_(self): return self.sessions._unload_() # def WebInterface_configuration_details(self, **kwargs): # return [{'webinterface': { # 'enabled': { # 'description': { # 'en': 'Enables/disables the web interface.', # } # }, # 'port': { # 'description': { # 'en': 'Port number for the web interface to listen on.' # } # } # }, # }] def _configuration_set_(self, **kwargs): """ Receive configuruation updates and adjust as needed. :param kwargs: section, option(key), value :return: """ section = kwargs['section'] option = kwargs['option'] value = kwargs['value'] if section == 'core': if option == 'label': self.misc_wi_data['gateway_label'] = value def i18n(self, request): """ Gets a translator based on the language the browser provides us. :param request: The browser request. :return: """ return self._Localize.get_ugettext(self._Localize.parse_accept_language(request.getHeader('accept-language'))) def _module_prestart_(self, **kwargs): """ Called before modules have their _prestart_ function called. This implements the hook "webinterface_add_routes" and calls all libraries and modules. It allows libs and modules to add menus to the web interface and provide additional funcationality. **Usage**: .. code-block:: python def ModuleName_webinterface_add_routes(self, **kwargs): return { 'nav_side': [ { 'label1': 'Tools', 'label2': 'MQTT', 'priority1': 3000, 'priority2': 10000, 'icon': 'fa fa-wrench fa-fw', 'url': '/tools/mqtt', 'tooltip': '', 'opmode': 'run', }, ], 'routes': [ self.web_interface_routes, ], } """ # first, lets get the top levels already defined so children don't re-arrange ours. temp_dict = {} newlist = sorted(nav_side_menu, key=itemgetter('priority1', 'priority2')) for item in newlist: level1 = item['label1'] if level1 not in newlist: temp_dict[level1] = item['priority1'] temp_strings = yombo.utils.global_invoke_all('_webinterface_add_routes_') # print "new routes: %s" % temp_strings for component, options in temp_strings.iteritems(): # print "1111" if 'nav_side' in options: # print "1111 2" for new_nav in options['nav_side']: # print "1111 3" if new_nav['label1'] in temp_dict: # print "1111 4" new_nav['priority1'] = temp_dict[new_nav['label1']] nav_side_menu.append(new_nav) if 'routes' in options: for new_route in options['routes']: new_route(self.webapp) self.misc_wi_data['nav_side'] = OrderedDict() newlist = sorted(nav_side_menu, key=itemgetter('priority1', 'priority2')) for item in newlist: level1 = item['label1'] if level1 not in self.misc_wi_data['nav_side']: self.misc_wi_data['nav_side'][level1] = [] self.misc_wi_data['nav_side'][level1].append(item) # print self.misc_wi_data['nav_side'] def add_alert(self, message, level='info', dismissable=True, type='session', deletable=True): """ Add an alert to the stack. :param level: info, warning, error :param message: :return: """ rand = yombo.utils.random_string(length=12) self.alerts[rand] = { 'type': type, 'level': level, 'message': message, 'dismissable': dismissable, 'deletable': deletable, } return rand def make_alert(self, message, level='info', type='session', dismissable=False): """ Add an alert to the stack. :param level: info, warning, error :param message: :return: """ return { 'level': level, 'message': message, 'dismissable': dismissable, } def get_alerts(self, type=None, session=None): """ Retrieve a list of alerts for display. """ if type is None: type = 'session' show_alerts = OrderedDict() for keyid in self.alerts.keys(): if self.alerts[keyid]['type'] == type: show_alerts[keyid] = self.alerts[keyid] if type == 'session': del self.alerts[keyid] return show_alerts def get_template(self, request, template_path): request.setHeader('server', 'Yombo/1.0') return self.webapp.templates.get_template(template_path) def redirect(self, request, redirect_path): request.setHeader('server', 'Yombo/1.0') request.redirect(redirect_path) def check_op_mode(self, request, router, **kwargs): # print "op mode: %s" % self._op_mode if self._op_mode == 'config': print "showing config home" method = getattr(self, 'config_'+ router) return method(request, **kwargs) elif self._op_mode == 'firstrun': method = getattr(self, 'firstrun_'+ router) return method(request, **kwargs) method = getattr(self, 'run_'+ router) return method(request, **kwargs) @webapp.route('/') @run_first() def home(self, request): return self.check_op_mode(request, 'home') @require_auth() def run_home(self, request, session): page = self.webapp.templates.get_template(self._dir + 'pages/index.html') i18n = self.i18n(request) return page.render(alerts=self.get_alerts(), delay_commands = self._Devices.delay_queue_active, automation_rules = len(self._Loader.loadedLibraries['automation'].rules), devices=self._Libraries['devices']._devicesByUUID, modules=self._Libraries['modules']._modulesByUUID, states=self._Libraries['states'].get_states(), _=i18n, ) @require_auth() def config_home(self, request, session): # auth = self.require_auth(request) # if auth is not None: # return auth page = self.get_template(request, self._dir + 'config_pages/index.html') return page.render(alerts=self.get_alerts(), ) def firstrun_home(self, request): return self.redirect(request, '/setup_wizard/1') @webapp.route('/logout', methods=['GET']) @run_first() def page_logout_get(self, request): # print "logout" self.sessions.close_session(request) request.received_cookies[self.sessions.config.cookie_session] = 'LOGOFF' return self.home(request) @webapp.route('/login/user', methods=['GET']) @require_auth_pin() def page_login_user_get(self, request): return self.redirect(request, '/') @webapp.route('/login/user', methods=['POST']) @require_auth_pin() @inlineCallbacks def page_login_user_post(self, request): submitted_email = request.args.get('email')[0] submitted_password = request.args.get('password')[0] # if submitted_pin.isalnum() is False: # alerts = { '1234': self.make_alert('Invalid authentication.', 'warning')} # return self.require_auth(request, alerts) if self._op_mode != 'firstrun': results = yield self._LocalDb.get_gateway_user_by_email(self.gwid, submitted_email) if len(results) != 1: self.add_alert('Email address not allowed to access gateway.', 'warning') # self.sessions.load(request) page = self.get_template(request, self._dir + 'pages/login_user.html') returnValue(page.render(alerts=self.get_alerts(), ) ) results = yield self.api.user_login_with_credentials(submitted_email, submitted_password) if results is not False: # if submitted_email == 'one' and submitted_password == '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b': session = self.sessions.load(request) if session is False: session = self.sessions.create(request) session['auth'] = True session['auth_id'] = submitted_email session['auth_time'] = time() session['yomboapi_session'] = results['session'] session['yomboapi_login_key'] = results['login_key'] request.received_cookies[self.sessions.config.cookie_session] = session.session_id if self._op_mode == 'firstrun': # print "###############33 saving system session stufff...." self.api.save_system_login_key(results['login_key']) # print "###############33 saving system session stufff....done" self.api.save_system_session(results['session']) else: self.add_alert('Invalid login credentails', 'warning') # self.sessions.load(request) page = self.get_template(request, self._dir + 'pages/login_user.html') returnValue(page.render(alerts=self.get_alerts(), ) ) print "session: %s" % session login_redirect = "/" if 'login_redirect' in session: login_redirect = session['login_redirect'] # print "$$$$$$$$$$$$$$$$$$ login redirect is set..." session.delete('login_redirect') # print "111 login_rdirect: %s" % login_redirect # print "delete login redirect... %s" % self.sessions.delete(request, 'login_redirect') # print "login/user:login_redirect: %s" % login_redirect # print "after delete rediret...session: %s" % session returnValue(self.redirect(request, login_redirect)) @webapp.route('/login/pin', methods=['POST']) @run_first() def page_login_pin_post(self, request): submitted_pin = request.args.get('authpin')[0] valid_pin = False print "pin submit: %s" % submitted_pin if submitted_pin.isalnum() is False: print "pin submit2: %s" % submitted_pin self.add_alert('Invalid authentication.', 'warning') return self.redirect(request, '/login/pin') print "pin submit2: %s" % submitted_pin if self.auth_pin_type == 'pin': if submitted_pin == self.auth_pin: # print "pin post444" expires = 10 * 365 * 24 * 60 * 60 # 10 years from now. request.addCookie(self.sessions.config.cookie_pin, time(), domain=None, path='/', secure=self.sessions.config.secure, httpOnly=self.sessions.config.httponly, max_age=expires) request.received_cookies[self.sessions.config.cookie_pin] = '1' session = self.sessions.create(request) session['auth'] = False session['auth_id'] = '' session['auth_time'] = 0 session['yomboapi_session'] = '' session['yomboapi_login_key'] = '' request.received_cookies[self.sessions.config.cookie_session] = session.session_id # print "session: %s" % session else: return self.redirect(request, '/login/pin') return self.home(request) @webapp.route('/login/pin', methods=['GET']) @require_auth() def page_login_pin_get(self, request): return self.redirect(request, '/') def restart(self, request, message=None, redirect=None): if message is None: message = "Web interface requested restart." if redirect is None: redirect = "/" page = self.get_template(request, self._dir + 'pages/restart.html') reactor.callLater(0.3, self.do_restart) return page.render(message=message, redirect=redirect, uptime=str(self._Atoms['running_since']) ) def do_restart(self): try: raise YomboRestart("Web Interface setup wizard complete.") except: pass def shutdown(self, request): page = self.get_template(request, self._dir + 'pages/shutdown.html') # reactor.callLater(0.3, self.do_shutdown) return page.render() def do_shutdown(self): try: raise YomboCritical("Web Interface setup wizard complete.") except: pass @webapp.route('/static/', branch=True) @run_first() def static(self, request): return File(self._current_dir + "/lib/webinterface/static/dist") def display_pin_console(self): local = "http://localhost:%s" % self.wi_port_nonsecure internal = "http://%s:%s" %(self._Configs.get('core', 'localipaddress'), self.wi_port_nonsecure) external = "http://%s:%s" % (self._Configs.get('core', 'externalipaddress'), self.wi_port_nonsecure) print "###########################################################" print "# #" if self._op_mode != 'run': print "# The Yombo Gateway website is running in #" print "# configuration only mode. #" print "# #" print "# The website can be accessed from the following urls: #" print "# #" print "# On local machine: #" print "# %-54s #" % local print "# #" print "# On local network: #" print "# %-54s #" % internal print "# #" print "# From external network (check port forwarding): #" print "# %-54s #" % external print "# #" print "# #" print "# Web Interface access pin code: #" print "# %-25s #" % self.auth_pin print "# #" print "###########################################################" def _tpl_home_gateway_configured(self): if not self._home_gateway_configured(): return "This gateway is not properly configured. Click _here_ to run the configuration wizard." else: return "" def _home_gateway_configured(self): gwuuid = self._Configs.get("core", "gwuuid", None) gwhash = self._Configs.get("core", "gwhash", None) gpgkeyid = self._Configs.get('gpg', 'keyid', None) if gwuuid is None or gwhash is None or gpgkeyid is None: return False else: return True def _get_parms(self, request): return parse_qs(urlparse(request.uri).query) def epoch_to_human(self, the_time, format=None): if format is None: format = '%b %d %Y %H:%M:%S %Z' return strftime(format, localtime(the_time)) def format_markdown(webinterface, description, description_formatting): if description_formatting == 'restructured': return publish_parts(description, writer_name='html')['html_body'] elif description_formatting == 'markdown': return markdown.markdown(description, extensions=['markdown.extensions.nl2br', 'markdown.extensions.codehilite']) return description def make_link(self, link, link_text, target = None): if link == '' or link is None or link.lower() == "None": return "None" if target is None: target = "_self" return '<a href="%s" target="%s">%s</a>' % (link, target, link_text) def request_get_default(self, request, name, default, offset=None): if offset == None: offset = 0 try: return request.args.get(name)[offset] except: return default def add_breadcrumb(self, request, url, text, show = False): if hasattr(request, 'breadcrumb') is False: request.breadcrumb = [] self.misc_wi_data['breadcrumb'] = request.breadcrumb request.breadcrumb.append({'url': url, 'text': text, 'show': show}) def setup_basic_filters(self): self.webapp.templates.filters['yes_no'] = yombo.utils.is_yes_no self.webapp.templates.filters['make_link'] = self.make_link self.webapp.templates.filters['status_to_string'] = yombo.utils.status_to_string self.webapp.templates.filters['public_to_string'] = yombo.utils.public_to_string self.webapp.templates.filters['epoch_to_human'] = yombo.utils.epoch_to_string self.webapp.templates.filters['epoch_to_pretty_date'] = yombo.utils.pretty_date # yesterday, 5 minutes ago, etc. self.webapp.templates.filters['format_markdown'] = self.format_markdown def WebInterface_configuration_set(self, **kwargs): """ Hook from configuration library. Get any configuration changes. :param kwargs: 'section', 'option', and 'value' are sent here. :return: """ if kwargs['section'] == 'webinterface': option = kwargs['option'] if option == 'auth_pin': self.auth_pin = kwargs['value'] elif option == 'auth_pin_totp': self.auth_pin_totp = kwargs['value'] elif option == 'auth_pin_type': self.auth_pin_type = kwargs['value'] elif option == 'auth_pin_required': self.auth_pin_required = kwargs['value'] def _build_dist(self): """ This is blocking code. Doesn't really matter, it only does it on startup. Builds the 'dist' directory from the 'build' directory. Easy way to update the source css/js files and update the webinterface JS and CSS files. :return: """ if not path.exists('yombo/lib/webinterface/static/dist'): mkdir('yombo/lib/webinterface/static/dist') if not path.exists('yombo/lib/webinterface/static/dist/css'): mkdir('yombo/lib/webinterface/static/dist/css') if not path.exists('yombo/lib/webinterface/static/dist/js'): mkdir('yombo/lib/webinterface/static/dist/js') if not path.exists('yombo/lib/webinterface/static/dist/fonts'): mkdir('yombo/lib/webinterface/static/dist/fonts') def do_cat(inputs, output): output = 'yombo/lib/webinterface/static/' + output # print "Saving to %s..." % output with open(output, 'w') as outfile: for fname in inputs: fname = 'yombo/lib/webinterface/static/' + fname # print "...%s" % fname with open(fname) as infile: outfile.write(infile.read()) # print "" def copytree(src, dst, symlinks=False, ignore=None): src = 'yombo/lib/webinterface/static/' + src dst = 'yombo/lib/webinterface/static/' + dst if path.isdir(src): if not path.exists(dst): mkdir(dst) for item in listdir(src): s = path.join(src, item) d = path.join(dst, item) if path.isdir(s): shutil.copytree(s, d, symlinks, ignore) else: shutil.copy2(s, d) CAT_SCRIPTS = [ 'source/jquery/jquery-2.2.4.min.js', 'source/sb-admin/js/js.cookie.min.js', 'source/bootstrap/dist/js/bootstrap.min.js', 'source/metisMenu/metisMenu.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/jquery-cookie-bootstrap-metismenu.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/jquery/jquery.validate-1.15.0.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/jquery.validate-1.15.0.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/bootstrap/dist/css/bootstrap.min.css', 'source/metisMenu/metisMenu.min.css', ] CAT_SCRIPTS_OUT = 'dist/css/bootsrap-metisMenu.min.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/bootstrap/dist/css/bootstrap.min.css', ] CAT_SCRIPTS_OUT = 'dist/css/bootsrap.min.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/js/sb-admin-2.min.js', 'source/sb-admin/js/yombo.js', ] CAT_SCRIPTS_OUT = 'dist/js/sb-admin2.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/css/sb-admin-2.css', 'source/sb-admin/css/yombo.css', ] CAT_SCRIPTS_OUT = 'dist/css/admin2.min.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/font-awesome/css/font-awesome.min.css', ] CAT_SCRIPTS_OUT = 'dist/css/font_awesome.min.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css', 'source/datatables-responsive/css/responsive.dataTables.min.css', ] CAT_SCRIPTS_OUT = 'dist/css/datatables.min.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/datatables/js/jquery.dataTables.min.js', 'source/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js', 'source/datatables-responsive/js/dataTables.responsive.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/datatables.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/creative/js/jquery.easing.min.js', 'source/creative/js/scrollreveal.min.js', 'source/creative/js/creative.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/creative.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/creative/css/creative.css', ] CAT_SCRIPTS_OUT = 'dist/css/creative.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/js/mappicker.js', ] CAT_SCRIPTS_OUT = 'dist/js/mappicker.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/css/mappicker.css', ] CAT_SCRIPTS_OUT = 'dist/css/mappicker.css' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/echarts/echarts.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/echarts.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/js/sha256.js', ] CAT_SCRIPTS_OUT = 'dist/js/sha256.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/sb-admin/js/jquery.serializejson.min.js', ] CAT_SCRIPTS_OUT = 'dist/js/jquery.serializejson.min.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) CAT_SCRIPTS = [ 'source/yombo/jquery.are-you-sure.js', ] CAT_SCRIPTS_OUT = 'dist/js/jquery.are-you-sure.js' do_cat(CAT_SCRIPTS, CAT_SCRIPTS_OUT) # Just copy files copytree('source/font-awesome/fonts/', 'dist/fonts/') copytree('source/bootstrap/dist/fonts/', 'dist/fonts/')