diff --git a/Makefile b/Makefile index 6b29b092b..5135d4edc 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ default: config dirs template css docs all: default predepend: - sudo sh -c "apt-get install augeas-tools libpython2.7 pandoc psmisc python2.7 python-augeas python-bcrypt python-bjsonrpc python-cheetah python-cherrypy3 python-simplejson sudo" + sudo sh -c "apt-get install augeas-tools libpython2.7 pandoc psmisc python2.7 python-augeas python-passlib python-bcrypt python-bjsonrpc python-cheetah python-cherrypy3 python-simplejson python-contract sudo" git submodule init git submodule update touch predepend diff --git a/actions/pagekite-configure b/actions/pagekite-configure new file mode 100755 index 000000000..229c0532c --- /dev/null +++ b/actions/pagekite-configure @@ -0,0 +1,302 @@ +#!/usr/bin/python +# -*- mode: python -*- +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Configuration helper for Plint PageKite inteface + +TODO: Use augeas for manipulating /etc/pagekite.d/* files +""" + +# Disable warning about invalid module name # pylint: disable-msg=C0103 + +import argparse +import os +import re +import subprocess + +CONFIG_DIR = '/etc/pagekite.d' + +SERVICE_FILE_MAP = { + 'http': { + 'file': '80_httpd.rc', + 'match': 'http:', + 'line': 'service_on = http:@kitename : localhost:80 : @kitesecret'}, + 'ssh': { + 'file': '80_sshd.rc', + 'match': 'raw/22:', + 'line': 'service_on = raw/22:@kitename : localhost:22 : @kitesecret'}} + + +def parse_arguments(): + """Return parsed command line arguments as dictionary""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + # Get installed status + subparsers.add_parser('get-installed', + help='Get whether PakeKite is installed') + + # Start PageKite + subparsers.add_parser('start', help='Start PageKite service') + + # Stop PageKite + subparsers.add_parser('stop', help='Stop PageKite service') + + # Get status + subparsers.add_parser('get-status', help='Get whether PakeKite is enabled') + + # Set status + set_status = subparsers.add_parser('set-status', + help='Enable/disable PageKite') + set_status.add_argument('enable', choices=['enable', 'disable']) + + # Get kite details + subparsers.add_parser('get-kite', + help='Get configured kite name and secret') + + # Set kite details + set_kite = subparsers.add_parser('set-kite', + help='Configure kite name and its secret') + set_kite.add_argument('--kite-name', + help='Name of the kite (eg: mybox.pagekite.me)') + set_kite.add_argument('--kite-secret', help='Secret for the kite') + + # Get service status + get_service = subparsers.add_parser('get-service-status', + help='Get whether service is enabled') + get_service.add_argument('service', choices=['http', 'ssh']) + + # Set service status + set_service = subparsers.add_parser('set-service-status', + help='Enable/disable a service') + set_service.add_argument('service', choices=['http', 'ssh']) + set_service.add_argument('enable', choices=['enable', 'disable']) + + return parser.parse_args() + + +def subcommand_get_installed(_): + """Print whether PageKite is installed""" + with open('/dev/null', 'w') as file_handle: + status = subprocess.call(['which', 'pagekite'], stdout=file_handle) + + print 'installed' if not status else 'not installed' + + +def subcommand_start(_): + """Start PageKite service""" + status = subprocess.call(['service', 'pagekite', 'start']) + if status: + raise Exception('Unable to start PageKite server') + + +def subcommand_stop(_): + """Stop PageKite service""" + status = subprocess.call(['service', 'pagekite', 'stop']) + if status: + raise Exception('Unable to stop PageKite server') + + +def subcommand_get_status(_): + """Print status of the pagekite service""" + is_enabled = is_pagekite_enabled() + print 'enabled' if is_enabled else 'disabled' + + +def is_pagekite_enabled(): + """Return if pagekite is enabled""" + service_file_path = os.path.join(CONFIG_DIR, '10_account.rc') + try: + with open(service_file_path, 'r') as file_object: + for line in file_object: + regex = r'^[ \t]*abort_not_configured' + if re.match(regex, line): + return False + except Exception: + return True + + return True + + +def subcommand_set_status(arguments): + """Enable/disable the pagekite service""" + enable = arguments.enable == 'enable' + is_enabled = is_pagekite_enabled() + if enable and is_enabled: + print 'already enabled' + return + + if not enable and not is_enabled: + print 'already disabled' + return + + if enable: + pagekite_enable() + print 'enabled' + else: + pagekite_disable() + print 'disabled' + + +def pagekite_enable(): + """Enable the pagekite daemon""" + file_path = os.path.join(CONFIG_DIR, '10_account.rc') + file_path_new = os.path.join(CONFIG_DIR, '10_account.rc.new') + with open(file_path, 'r') as read_file_object, \ + open(file_path_new, 'w') as write_file_object: + for line in read_file_object: + if not re.match('^[ \t]*abort_not_configured', line): + write_file_object.write(line) + + os.rename(file_path_new, file_path) + + +def pagekite_disable(): + """Disable the pagekite daemon""" + file_path = os.path.join(CONFIG_DIR, '10_account.rc') + with open(file_path, 'a') as file_object: + file_object.write('abort_not_configured\n') + + +def subcommand_get_kite(_): + """Print details of the currently configure kite""" + kite_name = '' + kite_secret = '' + + file_path = os.path.join(CONFIG_DIR, '10_account.rc') + with open(file_path, 'r') as file_object: + for line in file_object: + match = re.match(r'[ \t]*kitename[ \t]*=[ \t]*(.*)', line) + if match: + kite_name = match.group(1) + continue + + match = re.match(r'[ \t]*kitesecret[ \t]*=[ \t]*(.*)', line) + if match: + kite_secret = match.group(1) + continue + + print kite_name + print kite_secret + + +def subcommand_set_kite(arguments): + """Set details of the kite""" + kite_name = arguments.kite_name + kite_secret = arguments.kite_secret + + file_path = os.path.join(CONFIG_DIR, '10_account.rc') + file_path_new = os.path.join(CONFIG_DIR, '10_account.rc.new') + with open(file_path, 'r') as read_file_object, \ + os.fdopen(os.open(file_path_new, os.O_WRONLY | os.O_CREAT, + 0400), 'w') as write_file_object: + for line in read_file_object: + if re.match(r'[ \t]*kitename[ \t]*=.*', line): + write_file_object.write( + 'kitename = {kite_name}\n'.format(kite_name=kite_name)) + continue + + if re.match(r'[ \t]*kitesecret[ \t]*=.*', line): + write_file_object.write('kitesecret = {kite_secret}\n' + .format(kite_secret=kite_secret)) + continue + + write_file_object.write(line) + + os.rename(file_path_new, file_path) + + +def subcommand_get_service_status(arguments): + """Print status of the pagekite service""" + is_enabled = is_service_enabled(arguments.service) + print 'enabled' if is_enabled else 'disabled' + + +def is_service_enabled(service): + """Return if a service is enabled""" + service = SERVICE_FILE_MAP[service] + service_file_path = os.path.join(CONFIG_DIR, service['file']) + if not os.path.isfile(service_file_path): + return False + + try: + with open(service_file_path, 'r') as file_object: + for line in file_object: + regex = '[ \t]*service_on[ \t]*=[ \t]*{match}' + regex = regex.format(match=service['match']) + if re.match(regex, line): + return True + except Exception: + return False + + return False + + +def subcommand_set_service_status(arguments): + """Enable/disable a pagekite service""" + enable = arguments.enable == 'enable' + is_enabled = is_service_enabled(arguments.service) + if enable and is_enabled: + print 'already enabled' + return + + if not enable and not is_enabled: + print 'already disabled' + return + + if enable: + service_enable(arguments.service) + print 'enabled' + else: + service_disable(arguments.service) + print 'disabled' + + +def service_enable(service_name): + """Enable a service""" + service = SERVICE_FILE_MAP[service_name] + service_file_path = os.path.join(CONFIG_DIR, service['file']) + with open(service_file_path, 'w') as file_object: + file_object.write(''' +# Expose the local {service_name} daemon +# File auto-generated by Plinth + +{line} +'''.format(service_name=service_name, line=service['line'])) + + +def service_disable(service_name): + """Disable a service""" + service = SERVICE_FILE_MAP[service_name] + service_file_path = os.path.join(CONFIG_DIR, service['file']) + service_file_path_new = os.path.join(CONFIG_DIR, + service['file'] + '.plinthbak') + os.rename(service_file_path, service_file_path_new) + + +def main(): + """Parse arguments and perform all duties""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + +if __name__ == "__main__": + main() diff --git a/cfg.py b/cfg.py index 408d3df3d..01aeee2d7 100644 --- a/cfg.py +++ b/cfg.py @@ -8,8 +8,8 @@ def get_item(parser, section, name): try: return parser.get(section, name) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - print ("The config file {} does not contain the {}.{} option.".format( - parser[0], section, name)) + print ("Configuration does not contain the {}.{} option.".format( + section, name)) raise parser = SafeConfigParser( diff --git a/modules/installed/router/pagekite.py b/modules/installed/router/pagekite.py new file mode 100644 index 000000000..be767560b --- /dev/null +++ b/modules/installed/router/pagekite.py @@ -0,0 +1,291 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Plinth module for configuring PageKite service +""" + +import cherrypy +from gettext import gettext as _ + +import actions +import cfg +from forms import Form +from modules.auth import require +from plugin_mount import PagePlugin, FormPlugin +import re +import util + + +class PageKite(PagePlugin): + """PageKite menu entry and introduction page""" + order = 60 + + def __init__(self, *args, **kwargs): + PagePlugin.__init__(self, *args, **kwargs) + + self.register_page("router.setup.pagekite") + self.register_page("router.setup.pagekite.configure") + cfg.html_root.router.setup.menu.add_item( + "Public Visibility (PageKite)", "icon-flag", + "/router/setup/pagekite", 50) + + @cherrypy.expose + @require() + def index(self, **kwargs): + """Serve introcution page""" + del kwargs # Unused + + main = _(""" +

PageKite is a system for exposing FreedomBox services when you +don't have a direct connection to the Internet. You only need this +service if your FreedomBox services are unreachable from the rest of +the Internet. This includes the following situations:

+ + + +

PageKite works around NAT, firewalls and IP-address limitations by +using a combination of tunnels and reverse proxies. Currently, +exposing web server and SSH server are supported. An intermediary +server with direct Internet access is required. Currently, only +pagekite.net server is supported and you will need an account +there. In future, it might be possible to use your buddy's FreedomBox +for this.

+ +

Configure +PageKite

+""".format(server_dir=cfg.server_dir)) + + sidebar_right = _(''' +PageKite +

Configure +PageKite

'''.format(server_dir=cfg.server_dir)) + + return self.fill_template(title="Public Visibility (PageKite)", + main=main, sidebar_right=sidebar_right) + + +class configure(FormPlugin, PagePlugin): # pylint: disable-msg=C0103 + """Main configuration form""" + order = 65 + + url = ["/router/setup/pagekite/configure"] + + js = """ + +""" + + def get_status(self): + """ + Return the current status of PageKite configuration by + executing various actions. + """ + status = {} + + # Check if PageKite is installed + output = self._run(['get-installed']) + cfg.log('Output - %s' % output) + if output.split()[0] != 'installed': + return None + + # PageKite service enabled/disabled + output = self._run(['get-status']) + status['enabled'] = (output.split()[0] == 'enabled') + + # PageKite kite details + output = self._run(['get-kite']) + kite_details = output.split() + status['kite_name'] = kite_details[0] + status['kite_secret'] = kite_details[1] + + # Service status + status['service'] = {} + for service in ('http', 'ssh'): + output = self._run(['get-service-status', service]) + status['service'][service] = (output.split()[0] == 'enabled') + + return status + + def main(self, *args, **kwargs): + """Build and return the main content area which is the form""" + del args # unused + + status = self.get_status() + + if not status: + return _(''' +

PageKite is not installed, please install it. PageKite comes +pre-installed with FreedomBox. On any Debian based system (such as +FreedomBox) you may install it using the command 'aptitude install +pagekite'

''') + + try: + message = kwargs['message'].text + except KeyError: + message = None + form = Form( + title="Configure PageKite", + action=cfg.server_dir + "/router/setup/pagekite/configure/", + name="configure_pagekite_form", message=message) + + form.checkbox(_("Enable PageKite"), name="pagekite_enable", + id="pagekite-enable", checked=status['enabled']) + + show_form = "block" if status['enabled'] else "none" + form.html(''' +
'''.format(show_form=show_form)) + + form.html(_("

PageKite Account

")) + form.text_input(_("Server"), name="pagekite_server", + id="pagekite-server", value="pagekite.net") + form.text_input(_("Kite name"), name="pagekite_kite_name", + id="pagekite-kite-name", value=status['kite_name']) + form.text_input(_("Kite secret"), name="pagekite_kite_secret", + id="pagekite-kite-secret", value=status['kite_secret']) + + form.html(_("

Services

")) + form.checkbox(_("Web Server (HTTP)"), name="pagekite_http_enable", + id="pagekite-http-enable", + checked=status['service']['http']) + form.checkbox(_("Secure Shell (SSH)"), name="pagekite_ssh_enable", + id="pagekite-ssh-enable", + checked=status['service']['ssh']) + + form.html("
") # pagekite-post-enable-form + + form.submit(_("Update setup")) + return form.render() + + def process_form(self, **kwargs): + """Handle form submission""" + status = self.get_status() + + message, new_status = self.validate_form(**kwargs) + if not message.text: + self.apply_changes(status, new_status, message) + + return message + + @staticmethod + def validate_form(**kwargs): + """Check whether all the input form values are correct""" + new_status = {} + message = util.Message() + + domain_name_re = r'^[\w-]{1,63}(\.[\w-]{1,63})*$' + pagekite_kite_name = kwargs.get('pagekite_kite_name', '').strip() + if not re.match(domain_name_re, pagekite_kite_name): + message.add(_('Invalid kite name')) + else: + new_status['kite_name'] = pagekite_kite_name + + pagekite_kite_secret = kwargs.get('pagekite_kite_secret', '').strip() + if not pagekite_kite_secret: + message.add(_('Invalid kite secret')) + else: + new_status['kite_secret'] = pagekite_kite_secret + + new_status['enabled'] = (kwargs.get('pagekite_enable') == 'on') + new_status['service'] = { + 'http': (kwargs.get('pagekite_http_enable') == 'on'), + 'ssh': (kwargs.get('pagekite_ssh_enable') == 'on') + } + + return message, new_status + + def apply_changes(self, old_status, new_status, message): + """Apply the changes to PageKite configuration""" + cfg.log.info('New status is - %s' % new_status) + + if old_status != new_status: + self._run(['stop']) + + if old_status['enabled'] != new_status['enabled']: + if new_status['enabled']: + self._run(['set-status', 'enable']) + message.add(_('PageKite enabled')) + else: + self._run(['set-status', 'disable']) + message.add(_('PageKite disabled')) + + if old_status['kite_name'] != new_status['kite_name'] or \ + old_status['kite_secret'] != new_status['kite_secret']: + self._run(['set-kite', '--kite-name', new_status['kite_name'], + '--kite-secret', new_status['kite_secret']]) + message.add(_('Kite details set')) + + for service, old_value in old_status['service'].items(): + if old_value != new_status['service'][service]: + if new_status['service'][service]: + self._run(['set-service-status', service, 'enable']) + message.add(_('Service enabled: {service}') + .format(service=service)) + else: + self._run(['set-service-status', service, 'disable']) + message.add(_('Service disabled: {service}') + .format(service=service)) + + if old_status != new_status: + self._run(['start']) + + @staticmethod + def _run(arguments, superuser=True): + """Run an given command and raise exception if there was an error""" + command = 'pagekite-configure' + + cfg.log.info('Running command - %s, %s, %s' % (command, arguments, + superuser)) + + if superuser: + output, error = actions.superuser_run(command, arguments) + else: + output, error = actions.run(command, arguments) + + if error: + raise Exception('Error setting/getting PageKite confguration - %s' + % error) + + return output diff --git a/modules/installed/router/router.py b/modules/installed/router/router.py index 278309f7b..6c1f42df0 100644 --- a/modules/installed/router/router.py +++ b/modules/installed/router/router.py @@ -88,19 +88,19 @@ class wan(FormPlugin, PagePlugin): url = ["/router/setup"] order = 10 - js = """""" + $(document).ready(function() { + $('#connect_type').change(hideshow_static); + hideshow_static(); + }); + })(jQuery); +""" def sidebar_right(self, *args, **kwargs): side='' @@ -141,7 +141,7 @@ class wan(FormPlugin, PagePlugin): action=cfg.server_dir + "/router/setup/wan/index", name="wan_connection_form", message=message) - form.dropdown('Connection Type', vals=["DHCP", "Static IP"], id="connect_type", onchange="hideshow_static()") + form.dropdown('Connection Type', vals=["DHCP", "Static IP"], id="connect_type") form.html('
') form.dotted_quad("WAN IP Address", name="wan_ip", quad=[wan_ip0, wan_ip1, wan_ip2, wan_ip3]) form.dotted_quad("Subnet Mask", name="subnet", quad=[subnet0, subnet1, subnet2, subnet3]) @@ -150,11 +150,6 @@ class wan(FormPlugin, PagePlugin): form.dotted_quad("Static DNS 2", name="dns2", quad=[dns20, dns21, dns22, dns23]) form.dotted_quad("Static DNS 3", name="dns3", quad=[dns30, dns31, dns32, dns33]) form.html('
') - form.html(""" """) form.submit("Set Wan") return form.render() diff --git a/modules/pagekite.py b/modules/pagekite.py new file mode 120000 index 000000000..2981955ca --- /dev/null +++ b/modules/pagekite.py @@ -0,0 +1 @@ +installed/router/pagekite.py \ No newline at end of file diff --git a/plugin_mount.py b/plugin_mount.py index cf0f26880..4983ea28a 100644 --- a/plugin_mount.py +++ b/plugin_mount.py @@ -140,7 +140,7 @@ class FormPlugin(): """If the user has tried to fill in the form, process it, otherwise, just display a default form.""" if kwargs: kwargs['message'] = self.process_form(**kwargs) - parts = get_parts(self) + parts = get_parts(self, **kwargs) return self.fill_template(**parts) def process_form(self, **kwargs): diff --git a/templates/base.tmpl b/templates/base.tmpl index 5ec88d4c4..ebfe48db6 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -96,7 +96,6 @@ - $js $main_menu_js $sub_menu_js + $js