From cf96797040be510006ada43b23be72af556638ff Mon Sep 17 00:00:00 2001 From: fonfon Date: Tue, 6 Jan 2015 16:32:10 +0100 Subject: [PATCH 01/21] pagekite action: Use augeas pagekite lens And allow setting any server as pagekite frontend. --- actions/__init__.py | 0 actions/pagekite | 296 +++++++++++++++ actions/pagekite-configure | 290 --------------- actions/pagekite-configure-configobj | 352 ++++++++++++++++++ actions/pagekite_common.py | 52 +++ data/usr/share/augeas/lenses/pagekite.aug | 79 ++++ .../augeas/lenses/tests/test_pagekite.aug | 111 ++++++ plinth/modules/pagekite/pagekite.py | 23 +- .../templates/pagekite_configure.html | 10 +- setup.py | 4 + 10 files changed, 919 insertions(+), 298 deletions(-) create mode 100644 actions/__init__.py create mode 100755 actions/pagekite delete mode 100755 actions/pagekite-configure create mode 100755 actions/pagekite-configure-configobj create mode 100644 actions/pagekite_common.py create mode 100644 data/usr/share/augeas/lenses/pagekite.aug create mode 100644 data/usr/share/augeas/lenses/tests/test_pagekite.aug diff --git a/actions/__init__.py b/actions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/actions/pagekite b/actions/pagekite new file mode 100755 index 000000000..561a31427 --- /dev/null +++ b/actions/pagekite @@ -0,0 +1,296 @@ +#!/usr/bin/python2 +# -*- 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 interface + +Unfortunately there is no python3 package for augeas yet +""" + +import argparse +import augeas +import os +import subprocess + +from pagekite_common import SERVICE_PARAMS, construct_params + +aug = augeas.Augeas() + +CONF_PATH = '/files/etc/pagekite.d' +PATHS = { + 'service_on': os.path.join(CONF_PATH, '*', 'service_on', '*'), + 'kitename': os.path.join(CONF_PATH, '10_account.rc', 'kitename'), + 'kitesecret': os.path.join(CONF_PATH, '10_account.rc', 'kitesecret'), + 'abort_not_configured': os.path.join(CONF_PATH, '10_account.rc', + 'abort_not_configured'), + 'defaults': os.path.join(CONF_PATH, '20_frontends.rc', 'defaults'), + 'frontend': os.path.join(CONF_PATH, '20_frontends.rc', 'frontend'), +} + + +def parse_arguments(): + """Return parsed command line arguments as dictionary""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + # Start/Stop PageKite + subparsers.add_parser('start', help='Start PageKite service') + subparsers.add_parser('stop', help='Stop PageKite service') + + # Get/set status + subparsers.add_parser('is-enabled', help='Get whether PakeKite is enabled') + subparsers.add_parser('enable', help='Enable PageKite service') + subparsers.add_parser('disable', help='Disable PageKite service') + + # get/set using the default pagekite.net frontend + subparsers.add_parser('get-pagekitenet-frontend-status', + help='Get whether pagekite.net frontend is enabled') + subparsers.add_parser('enable-pagekitenet-frontend', + help='Enable using default pagekite.net frontend') + subparsers.add_parser('disable-pagekitenet-frontend', + help='Disable default pagekite.net frontend') + + # Frontend + subparsers.add_parser('get-frontend', help='Get pagekite frontend') + set_frontend = subparsers.add_parser('set-frontend', + help='Set pagekite frontend') + set_frontend.add_argument('url', help='frontend url') + + # Kite details (name + secret) + subparsers.add_parser('get-kite', + help='Get configured kite name and secret') + 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') + + # Services + subparsers.add_parser('get-services', help='Get list of enabled services') + add_service = subparsers.add_parser('add-service', + help='Add a pagekite service') + add_service.add_argument('--params', help='\':\'-separated parameters') + remove_service = subparsers.add_parser('remove-service', + help='Remove a pagekite service') + remove_service.add_argument('--params', help='\':\'-separated parameters') + + return parser.parse_args() + + +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_is_enabled(_): + """Print whether pagekite is enabled (yes or no)""" + is_enabled = is_pagekite_enabled() + print 'yes' if is_enabled else 'no' + + +def is_pagekite_enabled(): + return not bool(aug.match(PATHS['abort_not_configured'])) + + +def subcommand_enable(_): + pagekite_enable() + print 'enabled' + + +def subcommand_disable(_): + pagekite_disable() + print 'disabled' + + +def subcommand_get_frontend(_): + """Get pagekite frontend url""" + url = aug.get(PATHS['frontend']) + print url if url else "" + + +def subcommand_set_frontend(arguments): + """Set pagekite frontend url and disable default pagekite.net frontend""" + aug.remove(PATHS['defaults']) + aug.set(PATHS['frontend'], arguments.url) + aug.save() + + +def subcommand_get_pagekitenet_frontend_status(_): + match = aug.match(PATHS['defaults']) + print "enabled" if match else "disabled" + + +def subcommand_enable_pagekitenet_frontend(_): + """Enable using default pageket.net frontend + + This disables any other frontends. + """ + aug.set(PATHS['defaults'], '') + aug.remove(PATHS['frontend']) + aug.save() + print "enabled" + + +def subcommand_disable_pagekitenet_frontend(_): + aug.remove(PATHS['defaults']) + aug.save() + print "disabled" + + +def pagekite_enable(): + """Enable the pagekite daemon""" + aug.remove(PATHS['abort_not_configured']) + aug.save() + + +def pagekite_disable(): + """Disable the pagekite daemon""" + aug.set(PATHS['abort_not_configured'], '') + aug.save() + + +def subcommand_get_services(arguments): + """ lists all available (enabled) services """ + for match in aug.match(PATHS['service_on']): + print ":".join([aug.get(os.path.join(match, param)) for param in + SERVICE_PARAMS]) + + +def subcommand_remove_service(arguments): + """Searches and removes the service(s) that match all given parameters""" + params = construct_params(arguments.params) + paths = get_existing_service_paths(params) + # TODO: theoretically, everything to do here is: + # [aug.remove(path) for path in paths] + # but augeas won't let you save the changed files, and won't tell you why + for path in paths: + filepath = convert_augeas_path_to_filepath(path) + service_found = False + with open(filepath, 'r') as file: + lines = file.readlines() + for i, line in enumerate(lines): + if line.startswith('service_on') and \ + all(param in line for param in params.values()): + lines[i] = "" + service_found = True + break + if service_found: + with open(filepath, 'w') as file: + file.writelines(lines) + + +def get_existing_service_paths(params): + """Returns paths of existing services that match the given params""" + # construct an augeas query path with patterns like: + # */service_on/*[protocol='http'] + path = PATHS['service_on'] + for key, value in params.items(): + path += "[%s='%s']" % (key, value) + return aug.match(path) + + +def subcommand_add_service(arguments): + """Add one service""" + params = construct_params(arguments.params) + if get_existing_service_paths(params): + msg = "Service with the parameters %s already exists" + raise RuntimeError(msg % params) + + root = get_new_service_path(params['protocol']) + # TODO: after adding a service, augeas fails writing the config; + # so do it manually here + path = convert_augeas_path_to_filepath(root) + with open(path, 'a') as servicefile: + line = "service_on = %s" % arguments.params + servicefile.write(line) + + +def convert_augeas_path_to_filepath(augpath, prefix='/files', + suffix='service_on'): + """Convert an augeas service_on path to the actual file path""" + if augpath.startswith(prefix): + augpath = augpath.replace(prefix, "", 1) + + index = augpath.rfind(suffix) + if index: + augpath = augpath[:index] + return augpath.rstrip('/') + + +def get_new_service_path(protocol): + """Get the augeas path of a new service for a protocol + + This takes care of existing services using a /service_on/*/ query""" + root = get_augeas_servicefile_path(protocol) + new_index = len(aug.match(root + '/*')) + 1 + return os.path.join(root, str(new_index)) + + +def get_augeas_servicefile_path(protocol): + """Get the augeas path where a service for a protocol should be stored""" + if protocol == 'http': + relpath = '80_httpd.rc' + elif protocol == 'https': + relpath = '443_https.rc' + elif protocol == 'raw/22': + relpath = '22_ssh.rc' + elif protocol.startswith('raw'): + port = protocol.split('/')[1] + relpath = '%s.rc' % port + else: + raise ValueError('Unsupported protocol: %s' % protocol) + + return os.path.join(CONF_PATH, relpath, 'service_on') + + +def subcommand_get_kite(_): + """Print details of the currently configured kite""" + kitename = aug.get(PATHS['kitename']) + kitesecret = aug.get(PATHS['kitesecret']) + print kitename if kitename else '' + print kitesecret if kitesecret else '' + + +def subcommand_set_kite(arguments): + """Set details of the kite""" + aug.set(PATHS['kitename'], arguments.kite_name) + aug.set(PATHS['kitesecret'], arguments.kite_secret) + aug.save() + + +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/actions/pagekite-configure b/actions/pagekite-configure deleted file mode 100755 index a5af96fb0..000000000 --- a/actions/pagekite-configure +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/python3 -# -*- 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') - - # 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_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, - 0o400), '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/actions/pagekite-configure-configobj b/actions/pagekite-configure-configobj new file mode 100755 index 000000000..e1a9cde60 --- /dev/null +++ b/actions/pagekite-configure-configobj @@ -0,0 +1,352 @@ +#!/usr/bin/python3 +# -*- 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 interface + +Unfortunately there is no python3 package for augeas yet +""" + +# Disable warning about invalid module name # pylint: disable-msg=C0103 + +import argparse +#import augeas +from configobj import ConfigObj +import os +import subprocess + +#aug = augeas.Augeas() +aug = "XX" + +class ConfigFileCache(dict): + """Cache files to not open files more than once""" + def __missing__(self, path): + self[path] = ConfigObj(path) + return self[path] + +filecache = ConfigFileCache() + + +CONF_PATH = '/etc/pagekite.d' +PATHS = { + 'account': os.path.join(CONF_PATH, '10_account.rc'), + 'frontends': os.path.join(CONF_PATH, '20_frontends.rc'), + 'kitesecret': os.path.join(CONF_PATH, '10_account.rc', 'kitesecret'), + 'abort_not_configured': os.path.join(CONF_PATH, '10_account.rc', + 'abort_not_configured'), + 'defaults': os.path.join(CONF_PATH, '20_frontends.rc', 'defaults'), + 'frontend': os.path.join(CONF_PATH, '20_frontends.rc', 'frontend'), + 'http': os.path.join(CONF_PATH, '80_httpd.rc', 'service_on'), + 'https': os.path.join(CONF_PATH, '443_https.rc', 'service_on'), + 'ssh': os.path.join(CONF_PATH, '80_sshd.rc', 'service_on'), +} + + +_CONF_PATH = '/files/etc/pagekite.d' +_PATHS = { + 'kitename': os.path.join(CONF_PATH, '10_account.rc', 'kitename'), + 'kitesecret': os.path.join(CONF_PATH, '10_account.rc', 'kitesecret'), + 'abort_not_configured': os.path.join(CONF_PATH, '10_account.rc', + 'abort_not_configured'), + 'defaults': os.path.join(CONF_PATH, '20_frontends.rc', 'defaults'), + 'frontend': os.path.join(CONF_PATH, '20_frontends.rc', 'frontend'), + 'http': os.path.join(CONF_PATH, '80_httpd.rc', 'service_on'), + 'https': os.path.join(CONF_PATH, '443_https.rc', 'service_on'), + 'ssh': os.path.join(CONF_PATH, '80_sshd.rc', 'service_on'), +} +# service entries are tuples with [source, destination, secret] +# this information will be used when enabling a service +SERVICES = { + 'http': ['http:*.@kitename', 'localhost:80', '@kitesecret'], + 'https': ['https:*.@kitename', 'localhost:443', '@kitesecret'], + 'ssh': ['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') + + # 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 whether default pagekite.net frontend is enabled + subparsers.add_parser('get-pagekitenet-frontend-status', + help='Get whether pagekite.net frontend is enabled') + + # Enable/Disable using default pagekite.net frontend + subparsers.add_parser('enable-pagekitenet-frontend', + help='Enable using default pagekite.net frontend') + subparsers.add_parser('disable-pagekitenet-frontend', + help='Disable default pagekite.net frontend') + + # Get frontend + subparsers.add_parser('get-frontend', help='Get pagekite frontend') + + # Set frontend + set_frontend = subparsers.add_parser('set-frontend', + help='Set pagekite frontend') + set_frontend.add_argument('url', help='frontend url') + + # 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_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(): + conf = filecache[PATHS['account']] + return 'abort_not_configured' not in conf + + +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 subcommand_get_frontend(_): + """Get pagekite frontend url""" + import ipdb; ipdb.set_trace() + conf = filecache[PATHS['frontends']] + url = conf['frontend'] + #url = aug.get(PATHS['frontend']) + print(url if url else "") + + +def subcommand_set_frontend(arguments): + """Set pagekite frontend url and disable default pagekite.net frontend""" + aug.remove(PATHS['defaults']) + aug.set(PATHS['frontend'], arguments.url) + aug.save() + + +def subcommand_get_pagekitenet_frontend_status(_): + match = aug.match(PATHS['defaults']) + print("enabled" if match else "disabled") + + +def subcommand_enable_pagekitenet_frontend(_): + """Enable using default pageket.net frontend + + This disables any other frontends. + """ + aug.set(PATHS['defaults'], '') + aug.remove(PATHS['frontend']) + aug.save() + print("enabled") + + +def subcommand_disable_pagekitenet_frontend(_): + aug.remove(PATHS['defaults']) + aug.save() + print("disabled") + + +def pagekite_enable(): + """Enable the pagekite daemon""" + aug.remove(PATHS['abort_not_configured']) + aug.save() + + +def pagekite_disable(): + """Disable the pagekite daemon""" + aug.set(PATHS['abort_not_configured'], '') + aug.save() + + +def subcommand_get_service_status(arguments): + """Print status of the pagekite service""" + is_enabled = bool(get_enabled_service_paths(arguments.service)) + print('enabled' if is_enabled else 'disabled') + + +def subcommand_get_kite(_): + """Print details of the currently configured kite""" + kitename = aug.get(PATHS['kitename']) + kitesecret = aug.get(PATHS['kitesecret']) + print(kitename if kitename else '') + print(kitesecret if kitesecret else '') + + +def subcommand_set_kite(arguments): + """Set details of the kite""" + aug.set(PATHS['kitename'], arguments.kite_name) + aug.set(PATHS['kitesecret'], arguments.kite_secret) + aug.save() + + +def subcommand_set_service_status(arguments): + """Enable/disable a pagekite service""" + enable = arguments.enable == 'enable' + is_enabled = bool(get_enabled_service_paths(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): + """Enable a service""" + position = len(aug.match(PATHS[service])) + 1 + root = os.path.join(PATHS[service], str(position)) + set_service(root, *SERVICES[service]) + + +def set_service(root, source, destination, secret=None): + """Set service_on with the given augeas root (path)""" + aug.set(os.path.join(root, 'source'), source) + aug.set(os.path.join(root, 'destination'), destination) + if secret is not None: + aug.set(os.path.join(root, 'secret'), secret) + aug.save() + + +def get_enabled_service_paths(service): + """Search all service_on lines of the given protocol""" + paths = [] + for i, match in enumerate(aug.match(PATHS[service]), start=1): + service_path = os.path.join(match, str(i)) + source = aug.get(os.path.join(service_path, 'source')) + if service == "ssh" and "raw/22" in source: + paths.append(service_path) + elif service == "http" and service in source: + paths.append(service_path) + return paths + + +def service_disable(service): + """Disable a service""" + # TODO: saving config files after changing/deleting config entries with + # augeas fails with an IOError. + # Saving the file after removing this path does not work: + # /files/etc/pagekite.d/80_httpd.rc/service_on/1 + # Saving the file after removing this path works: + # /files/etc/pagekite.d/80_httpd.rc/service_on + # + # This could be an augeas bug, see + # http://permalink.gmane.org/gmane.comp.sysutils.augeas.devel/5419 + # Once this problem is solved, all you should need in this function is: + # + # paths = get_enabled_service_paths(service) + # [aug.remove(path) for path in paths] + # aug.save() + + if service == "ssh": + path = '/etc/pagekite.d/80_sshd.rc' + activated_pattern = 'raw/22' + elif service == "http": + path = '/etc/pagekite.d/80_httpd.rc' + activated_pattern = 'http' + with open(path, 'r') as file: + lines = file.readlines() + for i, line in enumerate(lines): + if line.startswith('service_on') and activated_pattern in line: + lines[i] = "#%s" % line + break + with open(path, 'w') as file: + file.writelines(lines) + + +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/actions/pagekite_common.py b/actions/pagekite_common.py new file mode 100644 index 000000000..df552041e --- /dev/null +++ b/actions/pagekite_common.py @@ -0,0 +1,52 @@ +#!/usr/bin/python2 +# -*- 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 . +# + +""" +The variables/functions defined here are used by both the action script +and the plinth pagekite module. + +Currently that's functionality for converting pagekite service_on strings like + "http:@kitename:localhost:80:@kitestring" +into parameter dictionaries and the other way round. +""" + +SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', + 'secret'] + + +def construct_params(string): + """ Convert a parameter string into a params dictionary""" + try: + params = dict(zip(SERVICE_PARAMS, string.split(':'))) + except: + msg = """params are expected to be a ':'-separated string + containing values for: %s , for example:\n"--params + http:@kitename:localhost:8000:@kitesecret" + """ + raise ValueError(msg % ", ".join(SERVICE_PARAMS)) + return params + + +def deconstruct_params(params): + """ Convert params into a ":"-separated parameter string """ + try: + paramstring = ":".join([params[param] for param in SERVICE_PARAMS]) + except KeyError: + raise ValueError("Could not parse params: %s " % params) + return paramstring diff --git a/data/usr/share/augeas/lenses/pagekite.aug b/data/usr/share/augeas/lenses/pagekite.aug new file mode 100644 index 000000000..83ced80bd --- /dev/null +++ b/data/usr/share/augeas/lenses/pagekite.aug @@ -0,0 +1,79 @@ +(* +Module: Pagekite + Parses /etc/pagekite.d/ + +Author: Michael Pimmer + +About: License + This file is licenced under the LGPL v2+, like the rest of Augeas. +*) + +module Pagekite = +autoload xfm + +(* View: lns *) + +(* Variables *) +let equals = del /[ \t]*=[ \t]*/ "=" +let neg2 = /[^# \n\t]+/ +let neg3 = /[^# \:\n\t]+/ +let eol = del /\n/ "\n" +(* Match everything from here to eol, cropping whitespace at both ends *) +let to_eol = /[^ \t\n](.*[^ \t\n])?/ + +(* A key followed by comma-separated values + k: name of the key + key_sep: separator between key and values + value_sep: separator between values + sto: store for values +*) +let key_csv_line (k:string) (key_sep:lens) (value_sep:lens) (sto:lens) = + [ key k . key_sep . [ seq k . sto ] . + [ seq k . value_sep . sto ]* . Util.eol ] + +(* entries for pagekite.d/10_account.rc *) +let domain = [ key "domain" . equals . store neg2 . Util.comment_or_eol ] +let frontend = Build.key_value_line ("frontend" | "frontends") + equals (store Rx.neg1) +let host = Build.key_value_line "host" equals (store Rx.ip) +let ports = key_csv_line "ports" equals Sep.comma (store Rx.integer) +let protos = key_csv_line "protos" equals Sep.comma (store Rx.word) + +(* entries for pagekite.d/20_frontends.rc *) +let kitesecret = Build.key_value_line "kitesecret" equals (store Rx.space_in) +let kv_frontend = Build.key_value_line ( "kitename" | "fe_certname" | + "ca_certs" | "tls_endpoint" ) + equals (store Rx.neg1) + +(* entries for services like 80_httpd.rc *) +let service_colon = del /[ \t]*:[ \t]*/ " : " +let service_on = [ key "service_on" . [ seq "service_on" . equals . + [ label "protocol" . store neg3 ] . service_colon . + [ label "kitename" . (store neg3) ] . service_colon . + [ label "backend_host" . (store neg3) ] . service_colon . + [ label "backend_port" . (store neg3) ] . service_colon . ( + [ label "secret" . (store Rx.no_spaces) . Util.eol ] | eol + ) ] ] + +let service_cfg = [ key "service_cfg" . equals . store to_eol . eol ] + +let flags = ( "defaults" | "isfrontend" | "abort_not_configured" | "insecure" ) + +let entries = Build.flag_line flags + | domain + | frontend + | host + | ports + | protos + | kv_frontend + | kitesecret + | service_on + | service_cfg + +let lns = ( entries | Util.empty | Util.comment )* + +(* View: filter *) +let filter = incl "/etc/pagekite.d/*.rc" + . Util.stdexcl + +let xfm = transform lns filter diff --git a/data/usr/share/augeas/lenses/tests/test_pagekite.aug b/data/usr/share/augeas/lenses/tests/test_pagekite.aug new file mode 100644 index 000000000..d32bc4f48 --- /dev/null +++ b/data/usr/share/augeas/lenses/tests/test_pagekite.aug @@ -0,0 +1,111 @@ +module Test_Pagekite = + +let conf1 = "# Use the pagekite.net service defaults. +defaults +" +test Pagekite.lns get conf1 = + { "#comment" = "Use the pagekite.net service defaults." } + { "defaults" } + + +let conf2 =" +frontends = pagekite.freedombox.me +ports=80,81 +" +test Pagekite.lns get conf2 = + { } + { "frontends" = "pagekite.freedombox.me" } + { "ports" + { "1" = "80" } + { "2" = "81" } } + + +let conf3 = "frontend=pagekite.freedombox.me +host=192.168.0.3 +" +test Pagekite.lns get conf3 = + { "frontend" = "pagekite.freedombox.me" } + { "host" = "192.168.0.3" } + + +let conf4 = "isfrontend +ports=80,443 +protos=http,https +domain=http,https:*.your.domain:MakeUpAPasswordHere +" +test Pagekite.lns get conf4 = + { "isfrontend" } + { "ports" + { "1" = "80" } + { "2" = "443" } } + { "protos" + { "1" = "http" } + { "2" = "https" } } + { "domain" = "http,https:*.your.domain:MakeUpAPasswordHere" } + +let conf_account = "kitename = my.freedombox.me +kitesecret = 0420 +# Delete this line! +abort_not_configured +" +test Pagekite.lns get conf_account = + { "kitename" = "my.freedombox.me" } + { "kitesecret" = "0420" } + { "#comment" = "Delete this line!" } + { "abort_not_configured" } + + +let conf_service = " +service_on = raw/22:@kitename : localhost:22 : @kitesecret +service_on=http:192.168.0.1:127.0.0.1:80: +service_on=https:yourhostname,fqdn:127.0.0.1:443: +" +test Pagekite.lns get conf_service = + { } + { "service_on" + { "1" + { "protocol" = "raw/22" } + { "kitename" = "@kitename" } + { "backend_host" = "localhost" } + { "backend_port" = "22" } + { "secret" = "@kitesecret" } + } + } + { "service_on" + { "2" + { "protocol" = "http" } + { "kitename" = "192.168.0.1" } + { "backend_host" = "127.0.0.1" } + { "backend_port" = "80" } + } + } + { "service_on" + { "3" + { "protocol" = "https" } + { "kitename" = "yourhostname,fqdn" } + { "backend_host" = "127.0.0.1" } + { "backend_port" = "443" } + } + } + + +let conf_encryption = " +frontend=frontend.your.domain:443 +fe_certname=frontend.your/domain +ca_certs=/etc/pagekite.d/site-cert.pem +tls_endpoint=frontend.your.domain:/path/to/frontend.pem +" +test Pagekite.lns get conf_encryption = + { } + { "frontend" = "frontend.your.domain:443" } + { "fe_certname" = "frontend.your/domain" } + { "ca_certs" = "/etc/pagekite.d/site-cert.pem" } + { "tls_endpoint" = "frontend.your.domain:/path/to/frontend.pem" } + + +let conf_service_cfg = "insecure +service_cfg = KITENAME.pagekite.me/80 : insecure : True +" +test Pagekite.lns get conf_service_cfg = + { "insecure" } + { "service_cfg" = "KITENAME.pagekite.me/80 : insecure : True" } diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py index 132d7fff7..af2b34bc0 100644 --- a/plinth/modules/pagekite/pagekite.py +++ b/plinth/modules/pagekite/pagekite.py @@ -73,9 +73,9 @@ class ConfigureForm(forms.Form): # pylint: disable-msg=W0232 server = forms.CharField( label=_('Server'), required=False, - help_text=_('Currently only pagekite.net server is supported'), - widget=forms.TextInput(attrs={'placeholder': 'pagekite.net', - 'disabled': 'disabled'})) + help_text=_('Select your pagekite.net server. Set "pagekite.net" to ' + 'use the default pagekite.net server'), + widget=forms.TextInput()) kite_name = TrimmedCharField( label=_('Kite name'), @@ -143,6 +143,15 @@ def get_status(): status['kite_name'] = kite_details[0] status['kite_secret'] = kite_details[1] + # PageKite server: 'pagekite.net' if flag 'defaults' is set, + # the value of 'frontend' otherwise + use_pagekitenet_server = _run(['get-pagekitenet-frontend-status']) + if "enabled" in use_pagekitenet_server: + value = 'pagekite.net' + elif "disabled" in use_pagekitenet_server: + value = _run(['get-frontend']) + status['server'] = value.replace('\n', '') + # Service status status['service'] = {} for service in ('http', 'ssh'): @@ -173,6 +182,14 @@ def _apply_changes(request, old_status, new_status): '--kite-secret', new_status['kite_secret']]) messages.success(request, _('Kite details set')) + if old_status['server'] != new_status['server']: + server = new_status['server'] + if server in ('defaults', 'default', 'pagekite.net'): + _run(['enable-pagekitenet-frontend']) + else: + _run(['set-frontend', server]) + messages.success(request, _('Pagekite server set')) + for service in ['http', 'ssh']: if old_status[service + '_enabled'] != \ new_status[service + '_enabled']: diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html index 0d737ba9b..0ef209ee7 100644 --- a/plinth/modules/pagekite/templates/pagekite_configure.html +++ b/plinth/modules/pagekite/templates/pagekite_configure.html @@ -30,13 +30,13 @@

PageKite Account

- {% include 'bootstrapform/field.html' with field=form.server %} - {% include 'bootstrapform/field.html' with field=form.kite_name %} - {% include 'bootstrapform/field.html' with field=form.kite_secret %} + {{ form.server|bootstrap_horizontal }} + {{ form.kite_name|bootstrap_horizontal }} + {{ form.kite_secret|bootstrap_horizontal }}

Services

- {% include 'bootstrapform/field.html' with field=form.http_enabled %} - {% include 'bootstrapform/field.html' with field=form.ssh_enabled %} + {{ form.http_enabled|bootstrap_horizontal }} + {{ form.ssh_enabled|bootstrap_horizontal }}
diff --git a/setup.py b/setup.py index 537b438f4..f71dcbc81 100755 --- a/setup.py +++ b/setup.py @@ -140,6 +140,10 @@ setuptools.setup( glob.glob(os.path.join('actions', '*'))), ('/usr/share/man/man1', ['doc/plinth.1']), ('/etc/plinth', ['data/etc/plinth/plinth.config']), + ('/usr/share/augeas/lenses', + ['data/usr/share/augeas/lenses/pagekite.aug']), + ('/usr/share/augeas/lenses/tests', + ['data/usr/share/augeas/lenses/tests/test_pagekite.aug']), ('/etc/plinth/modules-enabled', glob.glob(os.path.join('data/etc/plinth/modules-enabled', '*')))], From 1fc0064fd0e3217bfe929dbde66d2536b0b22cd2 Mon Sep 17 00:00:00 2001 From: fonfon Date: Wed, 14 Jan 2015 19:14:27 +0000 Subject: [PATCH 02/21] refactoring pagekite: configuration form works splitting the services to a separate page is not yet finished --- actions/pagekite | 23 +-- actions/pagekite_common.py | 52 ------ actions/pagekite_util.py | 104 +++++++++++ plinth/modules/pagekite/__init__.py | 10 +- plinth/modules/pagekite/forms.py | 169 ++++++++++++++++++ plinth/modules/pagekite/pagekite.py | 37 +--- .../templates/pagekite_configure.html | 13 +- .../templates/pagekite_custom_services.html | 95 ++++++++++ .../templates/pagekite_default_services.html | 55 ++++++ .../templates/pagekite_introduction.html | 8 +- plinth/modules/pagekite/urls.py | 13 +- plinth/modules/pagekite/util.py | 141 +++++++++++++++ plinth/modules/pagekite/views.py | 123 +++++++++++++ plinth/templatetags/plinth_extras.py | 19 ++ plinth/tests/test_pagekite_actions.py | 79 ++++++++ 15 files changed, 818 insertions(+), 123 deletions(-) delete mode 100644 actions/pagekite_common.py create mode 100644 actions/pagekite_util.py create mode 100644 plinth/modules/pagekite/forms.py create mode 100644 plinth/modules/pagekite/templates/pagekite_custom_services.html create mode 100644 plinth/modules/pagekite/templates/pagekite_default_services.html create mode 100644 plinth/modules/pagekite/util.py create mode 100644 plinth/modules/pagekite/views.py create mode 100644 plinth/tests/test_pagekite_actions.py diff --git a/actions/pagekite b/actions/pagekite index 561a31427..34bbaadaa 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -28,11 +28,11 @@ import augeas import os import subprocess -from pagekite_common import SERVICE_PARAMS, construct_params +from pagekite_util import SERVICE_PARAMS, construct_params, \ + deconstruct_params, get_augeas_servicefile_path, CONF_PATH aug = augeas.Augeas() -CONF_PATH = '/files/etc/pagekite.d' PATHS = { 'service_on': os.path.join(CONF_PATH, '*', 'service_on', '*'), 'kitename': os.path.join(CONF_PATH, '10_account.rc', 'kitename'), @@ -226,7 +226,7 @@ def subcommand_add_service(arguments): # so do it manually here path = convert_augeas_path_to_filepath(root) with open(path, 'a') as servicefile: - line = "service_on = %s" % arguments.params + line = "service_on = %s" % deconstruct_params(params) servicefile.write(line) @@ -251,23 +251,6 @@ def get_new_service_path(protocol): return os.path.join(root, str(new_index)) -def get_augeas_servicefile_path(protocol): - """Get the augeas path where a service for a protocol should be stored""" - if protocol == 'http': - relpath = '80_httpd.rc' - elif protocol == 'https': - relpath = '443_https.rc' - elif protocol == 'raw/22': - relpath = '22_ssh.rc' - elif protocol.startswith('raw'): - port = protocol.split('/')[1] - relpath = '%s.rc' % port - else: - raise ValueError('Unsupported protocol: %s' % protocol) - - return os.path.join(CONF_PATH, relpath, 'service_on') - - def subcommand_get_kite(_): """Print details of the currently configured kite""" kitename = aug.get(PATHS['kitename']) diff --git a/actions/pagekite_common.py b/actions/pagekite_common.py deleted file mode 100644 index df552041e..000000000 --- a/actions/pagekite_common.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python2 -# -*- 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 . -# - -""" -The variables/functions defined here are used by both the action script -and the plinth pagekite module. - -Currently that's functionality for converting pagekite service_on strings like - "http:@kitename:localhost:80:@kitestring" -into parameter dictionaries and the other way round. -""" - -SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', - 'secret'] - - -def construct_params(string): - """ Convert a parameter string into a params dictionary""" - try: - params = dict(zip(SERVICE_PARAMS, string.split(':'))) - except: - msg = """params are expected to be a ':'-separated string - containing values for: %s , for example:\n"--params - http:@kitename:localhost:8000:@kitesecret" - """ - raise ValueError(msg % ", ".join(SERVICE_PARAMS)) - return params - - -def deconstruct_params(params): - """ Convert params into a ":"-separated parameter string """ - try: - paramstring = ":".join([params[param] for param in SERVICE_PARAMS]) - except KeyError: - raise ValueError("Could not parse params: %s " % params) - return paramstring diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py new file mode 100644 index 000000000..9a258e7f9 --- /dev/null +++ b/actions/pagekite_util.py @@ -0,0 +1,104 @@ +#!/usr/bin/python2 +# -*- 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 . +# + +""" +The variables/functions defined here are used by both the action script +and the plinth pagekite module. + +For example the functionality to convert pagekite service_on strings like + "http:@kitename:localhost:80:@kitestring" +into parameter dictionaries and the other way round. And functions that we want +to be covered by tests. +""" +# ATTENTION: This file has to be both python2 and python3 compatible + +import os +import shlex +CONF_PATH = '/files/etc/pagekite.d' + +SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', + 'secret'] + + +def construct_params(string): + """ Convert a parameter string into a params dictionary""" + # The actions.py uses shlex.quote() to escape/quote malicious user input. + # That affects '*.@kitename', so the params string gets quoted. + # If the string is escaped and contains '*.@kitename', look whether shlex + # would still quote/escape the string when we remove '*.@kitename'. + + # TODO: use shlex only once augeas-python supports python3 + if hasattr(shlex, 'quote'): + quotefunction = shlex.quote + else: + import pipes + quotefunction = pipes.quote + + if string.startswith("'") and string.endswith("'"): + unquoted_string = string[1:-1] + error_msg = "The parameters contain suspicious characters: %s " + if '*.@kitename' in string: + unquoted_test_string = unquoted_string.replace('*.@kitename', '') + if unquoted_test_string == quotefunction(unquoted_test_string): + # no other malicious characters found, use the unquoted string + string = unquoted_string + else: + raise RuntimeError(error_msg % string) + else: + raise RuntimeError(error_msg % string) + + try: + params = dict(zip(SERVICE_PARAMS, string.split(':'))) + except: + msg = """params are expected to be a ':'-separated string containing + values for: %s , for example:\n"--params + http/8000:@kitename:localhost:8000:@kitesecret" + """ + raise ValueError(msg % ", ".join(SERVICE_PARAMS)) + return params + + +def deconstruct_params(params): + """ Convert params into a ":"-separated parameter string """ + try: + paramstring = ":".join([str(params[param]) for param in + SERVICE_PARAMS]) + except KeyError: + raise ValueError("Could not parse params: %s " % params) + return paramstring + + +def get_augeas_servicefile_path(protocol): + """Get the augeas path where a service for a protocol should be stored""" + if not protocol.startswith(("http", "https", "raw")): + raise ValueError('Unsupported protocol: %s' % protocol) + + try: + _protocol, port = protocol.split('/') + except ValueError: + if protocol == 'http': + relpath = '80_http.rc' + elif protocol == 'https': + relpath = '443_https.rc' + else: + raise ValueError('Unsupported protocol: %s' % protocol) + else: + relpath = '%s_%s.rc' % (port, _protocol) + + return os.path.join(CONF_PATH, relpath, 'service_on') diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py index 0d836a04f..b9ad5fb14 100644 --- a/plinth/modules/pagekite/__init__.py +++ b/plinth/modules/pagekite/__init__.py @@ -19,9 +19,17 @@ Plinth module to configure PageKite """ +from gettext import gettext as _ +from plinth import cfg from . import pagekite -from .pagekite import init __all__ = ['pagekite', 'init'] depends = ['plinth.modules.apps'] + + +def init(): + """Intialize the PageKite module""" + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(_('Public Visibility (PageKite)'), + 'glyphicon-flag', 'pagekite:index', 50) diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py new file mode 100644 index 000000000..f19c59a92 --- /dev/null +++ b/plinth/modules/pagekite/forms.py @@ -0,0 +1,169 @@ +# +# 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 . +# + +from gettext import gettext as _ +import logging + +from django import forms +from django.contrib import messages +from django.core import validators + +from actions.pagekite_util import deconstruct_params +from .util import PREDEFINED_SERVICES, _run, get_kite_details, KITE_NAME, \ + KITE_SECRET, BACKEND_HOST + +LOGGER = logging.getLogger(__name__) + + +class TrimmedCharField(forms.CharField): + """Trim the contents of a CharField""" + def clean(self, value): + """Clean and validate the field value""" + if value: + value = value.strip() + + return super(TrimmedCharField, self).clean(value) + + +class ConfigurationForm(forms.Form): + """Configure PageKite credentials and frontend""" + + enabled = forms.BooleanField(label=_('Enable PageKite'), + required=False) + + server = forms.CharField( + label=_('Server'), required=False, + help_text=_('Select your pagekite.net server. Set "pagekite.net" to ' + 'use the default pagekite.net server'), + widget=forms.TextInput()) + + kite_name = TrimmedCharField( + label=_('Kite name'), + help_text=_('Example: mybox1-myacc.pagekite.me'), + validators=[ + validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$', + _('Invalid kite name'))]) + + kite_secret = TrimmedCharField( + label=_('Kite secret'), + help_text=_('A secret associated with the kite or the default secret \ +for your account if no secret is set on the kite')) + + def save(self, request): + old = self.initial + new = self.cleaned_data + LOGGER.info('New status is - %s', new) + + if old != new: + _run(['stop']) + + if old['enabled'] != new['enabled']: + if new['enabled']: + _run(['enable']) + messages.success(request, _('PageKite enabled')) + else: + _run(['disable']) + messages.success(request, _('PageKite disabled')) + + if old['kite_name'] != new['kite_name'] or \ + old['kite_secret'] != new['kite_secret']: + _run(['set-kite', '--kite-name', new['kite_name'], + '--kite-secret', new['kite_secret']]) + messages.success(request, _('Kite details set')) + + if old['server'] != new['server']: + server = new['server'] + if server in ('defaults', 'default', 'pagekite.net'): + _run(['enable-pagekitenet-frontend']) + else: + _run(['set-frontend', server]) + messages.success(request, _('Pagekite server set')) + + if old != new: + _run(['start']) + + +class DefaultServiceForm(forms.Form): + """Constructs a form out of PREDEFINED_SERVICES""" + + def __init__(self, *args, **kwargs): + """Add the fields from PREDEFINED_SERVICES""" + super(DefaultServiceForm, self).__init__(*args, **kwargs) + kite = get_kite_details() + for name, service in PREDEFINED_SERVICES.items(): + if name in ('http', 'https'): + help_text = service['help_text'].format(kite['kite_name']) + else: + help_text = service['help_text'] + self.fields[name] = forms.BooleanField(label=service['label'], + help_text=help_text, + required=False) + + def save(self, request): + formdata = self.cleaned_data + for service in PREDEFINED_SERVICES.keys(): + if self.initial[service] != formdata[service]: + params = PREDEFINED_SERVICES[service]['params'] + param_line = deconstruct_params(params) + if formdata[service]: + _run(['add-service', '--params', param_line]) + messages.success(request, _('Service enabled: {service}') + .format(service=service)) + else: + _run(['remove-service', '--params', param_line]) + messages.success(request, _('Service disabled: {service}') + .format(service=service)) + + +class CustomServiceForm(forms.Form): + """Form to add/delete a custom service""" + choices = [("http", "http"), ("https", "https"), ("raw", "raw")] + protocol = forms.ChoiceField(choices=choices, label="protocol") + frontend_port = forms.IntegerField(min_value=0, max_value=65535, + label="external (frontend) port") + backend_port = forms.IntegerField(min_value=0, max_value=65535, + label="internal (freedombox) port") + subdomains = forms.BooleanField(label="Enable Subdomains", required=False) + + def prepare_user_input_for_storage(self, params): + """prepare the user input for being stored via the action""" + # set kitename and kitesecret if not already set + if 'kitename' not in params: + if 'subdomains' in params and params['subdomains']: + params['kitename'] = "*.%s" % KITE_NAME + else: + params['kitename'] = KITE_NAME + if 'secret' not in params: + params['secret'] = KITE_SECRET + + # condense protocol and frontend_port to one entry (protocol) + if 'frontend_port' in params: + if str(params['frontend_port']) not in params['protocol']: + params['protocol'] = "%s/%s" % (params['protocol'], + params['frontend_port']) + if 'backend_host' not in params: + params['backend_host'] = BACKEND_HOST + + return deconstruct_params(params) + + def save(self, request): + params = self.prepare_user_input_for_storage(self.cleaned_data) + _run(['add-service', '--params', params]) + + def delete(self, request): + params = self.prepare_user_input_for_storage(self.cleaned_data) + _run(['remove-service', '--params', params]) diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py index af2b34bc0..a22c961d9 100644 --- a/plinth/modules/pagekite/pagekite.py +++ b/plinth/modules/pagekite/pagekite.py @@ -19,49 +19,16 @@ Plinth module for configuring PageKite service """ -from django import forms -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.core import validators -from django.core.urlresolvers import reverse_lazy -from django.template.response import TemplateResponse -from gettext import gettext as _ -import logging - -from plinth import actions -from plinth import cfg -from plinth import package -LOGGER = logging.getLogger(__name__) - -subsubmenu = [{'url': reverse_lazy('pagekite:index'), - 'text': _('About PageKite')}, - {'url': reverse_lazy('pagekite:configure'), - 'text': _('Configure PageKite')}] -def init(): - """Intialize the PageKite module""" - menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Public Visibility (PageKite)'), - 'glyphicon-flag', 'pagekite:index', 50) -@login_required -def index(request): - """Serve introduction page""" - return TemplateResponse(request, 'pagekite_introduction.html', - {'title': _('Public Visibility (PageKite)'), - 'subsubmenu': subsubmenu}) -class TrimmedCharField(forms.CharField): - """Trim the contents of a CharField""" - def clean(self, value): - """Clean and validate the field value""" - if value: - value = value.strip() + + return super(TrimmedCharField, self).clean(value) diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html index 0ef209ee7..07c5c6066 100644 --- a/plinth/modules/pagekite/templates/pagekite_configure.html +++ b/plinth/modules/pagekite/templates/pagekite_configure.html @@ -27,20 +27,15 @@ {% include 'bootstrapform/field.html' with field=form.enabled %} -

PageKite Account

{{ form.server|bootstrap_horizontal }} {{ form.kite_name|bootstrap_horizontal }} {{ form.kite_secret|bootstrap_horizontal }} -

Services

- {{ form.http_enabled|bootstrap_horizontal }} - {{ form.ssh_enabled|bootstrap_horizontal }}
- - - + {% endblock %} @@ -52,9 +47,9 @@ $('#id_pagekite-enabled').change(function() { if ($('#id_pagekite-enabled').prop('checked')) { - $('#pagekite-post-enabled-form').show('slow'); + $('.pagekite-post-enabled-form').show('slow'); } else { - $('#pagekite-post-enabled-form').hide('slow'); + $('.pagekite-post-enabled-form').hide('slow'); } }); diff --git a/plinth/modules/pagekite/templates/pagekite_custom_services.html b/plinth/modules/pagekite/templates/pagekite_custom_services.html new file mode 100644 index 000000000..d865d9d40 --- /dev/null +++ b/plinth/modules/pagekite/templates/pagekite_custom_services.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} + +{% load plinth_extras %} + +{% block page_head %} + +{% endblock %} + +{% block content %} + +

Custom Services

+ +
+ +
+

Existing custom services

+ {% if not custom_services %} + You don't have any Custom Services enabled + {% endif %} +
+ {% for service in custom_services %} + {% create_pagekite_service_link service kite_name as service_link %} +
+ + + {% if service_link|slice:":4" == "http" %} + {{ service_link }} + {% else %} + {{ service_link }} + {% endif %} + + +
+
+ {% csrf_token %} + {{ service.form }} +
+ +
+
+ {% endfor %} +
+
+ +
+
+

Create a custom service

+ {{ form|bootstrap_horizontal:'col-lg-6' }} + {% csrf_token %} + +
+
+ +
+ +{% endblock %} + + diff --git a/plinth/modules/pagekite/templates/pagekite_default_services.html b/plinth/modules/pagekite/templates/pagekite_default_services.html new file mode 100644 index 000000000..906cfb4b6 --- /dev/null +++ b/plinth/modules/pagekite/templates/pagekite_default_services.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} + +{% block page_head %} + +{% endblock %} + +{% block content %} + +

Default Services

+ +

Exposing services makes them accessible and attackable from the evil + internet. Be cautious!

+ +
+ {{ form.http|bootstrap_horizontal:'col-lg-0' }} + {{ form.https|bootstrap_horizontal:'col-lg-0' }} + {{ form.ssh|bootstrap_horizontal:'col-lg-0' }} + {% csrf_token %} + +
+ +{% endblock %} + diff --git a/plinth/modules/pagekite/templates/pagekite_introduction.html b/plinth/modules/pagekite/templates/pagekite_introduction.html index 211ff20c7..23c7fe99c 100644 --- a/plinth/modules/pagekite/templates/pagekite_introduction.html +++ b/plinth/modules/pagekite/templates/pagekite_introduction.html @@ -42,10 +42,10 @@ 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 +exposing web server and SSH server are supported. You can use any +server that offers a pagekite service, for example +pagekite.net. +In future, it might be possible to use your buddy's {{ cfg.box_name }} for this.

diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py index 7fa6c9918..ebc457c63 100644 --- a/plinth/modules/pagekite/urls.py +++ b/plinth/modules/pagekite/urls.py @@ -20,10 +20,19 @@ URLs for the PageKite module """ from django.conf.urls import patterns, url +from .views import DefaultServiceView, CustomServiceView, ConfigurationView, \ + DeleteServiceView urlpatterns = patterns( # pylint: disable-msg=C0103 - 'plinth.modules.pagekite.pagekite', + 'plinth.modules.pagekite.views', url(r'^apps/pagekite/$', 'index', name='index'), - url(r'^apps/pagekite/configure/$', 'configure', name='configure'), + url(r'^apps/pagekite/configure/$', ConfigurationView.as_view(), + name='configure'), + url(r'^apps/pagekite/services/default$', DefaultServiceView.as_view(), + name='default-services'), + url(r'^apps/pagekite/services/custom$', CustomServiceView.as_view(), + name='custom-services'), + url(r'^apps/pagekite/services/custom/delete$', DeleteServiceView.as_view(), + name='delete-custom-service'), ) diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py new file mode 100644 index 000000000..a40ba106f --- /dev/null +++ b/plinth/modules/pagekite/util.py @@ -0,0 +1,141 @@ +# +# 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 . +# + +from gettext import gettext as _ +import logging + +from actions.pagekite_util import construct_params +from plinth import actions + +LOGGER = logging.getLogger(__name__) + +# defaults for the credentials; @kitename acts as a placeholder and is +# understood (and replaced with the actual kitename) by pagekite. +KITE_NAME = '@kitename' +KITE_SECRET = '@kitesecret' +BACKEND_HOST = 'localhost' +# predefined services show up in the PredefinedServiceForm as checkbox +PREDEFINED_SERVICES = { + 'http': { + 'params': {'protocol': 'http', + 'kitename': KITE_NAME, + 'backend_port': '80', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Web Server (HTTP)"), + 'help_text': _("Site will be available at " + "http://{0}"), + }, + 'https': { + 'params': {'protocol': 'https', + 'kitename': KITE_NAME, + 'backend_port': '443', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Web Server (HTTPS)"), + 'help_text': _("Site will be available at " + "https://{0}"), + }, + 'ssh': { + 'params': {'protocol': 'raw/22', + 'kitename': KITE_NAME, + 'backend_port': '22', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Secure Shell (SSH)"), + 'help_text': _("See SSH client setup " + "instructions") + }, +} + + +def get_kite_details(): + output = _run(['get-kite']) + kite_details = output.split() + return {'kite_name': kite_details[0], + 'kite_secret': kite_details[1]} + + +def prepare_params_for_display(params): + """Add extra information to display a custom service: + + - protocol is split into 'protocol' and 'frontend_port' + - we try to detect whether 'subdomains' are supported (as boolean) + """ + protocol = params['protocol'] + if '/' in protocol: + params['protocol'], params['frontend_port'] = protocol.split('/') + params['subdomains'] = params['kitename'].startswith('*.') + return params + + +def get_pagekite_config(): + """ + Return the current PageKite configuration by executing various actions. + """ + status = {} + + # PageKite service enabled/disabled + output = _run(['is-enabled']) + status['enabled'] = (output.split()[0] == 'yes') + + # PageKite kite details + status.update(get_kite_details()) + + # PageKite server: 'pagekite.net' if flag 'defaults' is set, + # the value of 'frontend' otherwise + use_pagekitenet_server = _run(['get-pagekitenet-frontend-status']) + if "enabled" in use_pagekitenet_server: + value = 'pagekite.net' + elif "disabled" in use_pagekitenet_server: + value = _run(['get-frontend']) + status['server'] = value.replace('\n', '') + + return status + + +def get_pagekite_services(): + """Get enabled services. Returns two values: + + 1. predefined services: {'http': False, 'ssh': True, 'https': True} + 2. custom services: [{'protocol': 'http', 'secret' 'nono', ..}, [..]} + """ + custom = [] + predefined = {} + # set all predefined services to 'disabled' by default + [predefined.update({proto: False}) for proto in PREDEFINED_SERVICES.keys()] + # now, search for the enabled ones + for serviceline in _run(['get-services']).split(): + params = construct_params(serviceline) + for name, predefined_service in PREDEFINED_SERVICES.items(): + if params == predefined_service['params']: + predefined[name] = True + break + else: + custom.append(prepare_params_for_display(params)) + return predefined, custom + + +def _run(arguments, superuser=True): + """Run a given command and raise exception if there was an error""" + command = 'pagekite' + + if superuser: + return actions.superuser_run(command, arguments) + else: + return actions.run(command, arguments) diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py new file mode 100644 index 000000000..370944b03 --- /dev/null +++ b/plinth/modules/pagekite/views.py @@ -0,0 +1,123 @@ +# +# 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 . +# + +from gettext import gettext as _ +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse, reverse_lazy +from django.http.response import HttpResponseRedirect +from django.template.response import TemplateResponse +from django.views.generic import View, TemplateView +from django.views.generic.edit import FormView + +from plinth import package +from .util import get_pagekite_config, get_pagekite_services, get_kite_details +from .forms import ConfigurationForm, DefaultServiceForm, CustomServiceForm + +subsubmenu = [{'url': reverse_lazy('pagekite:index'), + 'text': _('About PageKite')}, + {'url': reverse_lazy('pagekite:configure'), + 'text': _('Configure PageKite')}, + {'url': reverse_lazy('pagekite:default-services'), + 'text': _('Default Services')}, + {'url': reverse_lazy('pagekite:custom-services'), + 'text': _('Custom Services')}] + + +@login_required +def index(request): + """Serve introduction page""" + return TemplateResponse(request, 'pagekite_introduction.html', + {'title': _('Public Visibility (PageKite)'), + 'subsubmenu': subsubmenu}) + + +class ContextMixin(object): + """Mixin to add 'subsubmenu' and 'title' to the context.""" + def get_context_data(self, **kwargs): + """Use self.title and the module-level subsubmenu""" + context = super(ContextMixin, self).get_context_data(**kwargs) + context['title'] = getattr(self, 'title', '') + context['subsubmenu'] = subsubmenu + return context + + +class DeleteServiceView(View): + def post(self, request): + form = CustomServiceForm(request.POST) + if form.is_valid(): + form.delete(request) + return HttpResponseRedirect(reverse('pagekite:custom-services')) + + +class CustomServiceView(ContextMixin, TemplateView): + template_name = 'pagekite_custom_services.html' + + def get_context_data(self, *args, **kwargs): + context = super(CustomServiceView, self).get_context_data(*args, + **kwargs) + unused, custom_services = get_pagekite_services() + for service in custom_services: + service['form'] = CustomServiceForm(initial=service) + context['custom_services'] = custom_services + context.update(get_kite_details()) + return context + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + form = CustomServiceForm(prefix="custom") + context['form'] = form + return self.render_to_response(context) + + def post(self, request): + unused, custom_services = get_pagekite_services() + form = CustomServiceForm(request.POST, prefix="custom") + if form.is_valid(): + form.save(request) + form = CustomServiceForm(prefix="custom") + + context = self.get_context_data() + context['form'] = form + + return self.render_to_response(context) + + +class DefaultServiceView(ContextMixin, FormView): + template_name = 'pagekite_default_services.html' + title = 'PageKite Default Services' + form_class = DefaultServiceForm + success_url = reverse_lazy('pagekite:default-services') + + def get_initial(self): + return get_pagekite_services()[0] + + def form_valid(self, form): + form.save(self.request) + return super(DefaultServiceView, self).form_valid(form) + + +class ConfigurationView(ContextMixin, FormView): + template_name = 'pagekite_configure.html' + form_class = ConfigurationForm + prefix = 'pagekite' + success_url = reverse_lazy('pagekite:configure') + + def get_initial(self): + return get_pagekite_config() + + def form_valid(self, form): + form.save(self.request) + return super(ConfigurationView, self).form_valid(form) diff --git a/plinth/templatetags/plinth_extras.py b/plinth/templatetags/plinth_extras.py index e3def79a1..e454a8269 100644 --- a/plinth/templatetags/plinth_extras.py +++ b/plinth/templatetags/plinth_extras.py @@ -15,6 +15,7 @@ # along with this program. If not, see . # +import copy import os from django import template @@ -58,3 +59,21 @@ def show_subsubmenu(context, menu): """Mark the active menu item and display the subsubmenu""" menu = mark_active_menuitem(menu, context['request'].path) return {'subsubmenu': menu} + + +@register.assignment_tag +def create_pagekite_service_link(service, kite_name): + """Create a link (URL) out of a pagekite service + + Parameters: - service: the params dictionary + - kite_name: kite name (from the pagekite configuration) + """ + params = {'protocol': service['protocol']} + if 'subdomains' in service and service['subdomains']: + params['kite_name'] = "*.%s" % kite_name + else: + params['kite_name'] = kite_name + link = "{protocol}://{kite_name}".format(**params) + if 'frontend_port' in service and service['frontend_port']: + link = "%s:%s" % (link, service['frontend_port']) + return link diff --git a/plinth/tests/test_pagekite_actions.py b/plinth/tests/test_pagekite_actions.py new file mode 100644 index 000000000..9f03f5fdb --- /dev/null +++ b/plinth/tests/test_pagekite_actions.py @@ -0,0 +1,79 @@ +# +# 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 . +# + +import os +import unittest + +from actions.pagekite_util import get_augeas_servicefile_path, CONF_PATH, \ + construct_params, deconstruct_params + + +class TestPagekiteActions(unittest.TestCase): + # test-cases to convert parameter-strings into param dicts and back + _tests = [ + { + 'line': 'https/8080:*.@kitename:localhost:8080:@kitesecret', + 'params': {'kitename': '*.@kitename', 'backend_host': 'localhost', + 'secret': '@kitesecret', 'protocol': 'https/8080', + 'backend_port': '8080'} + }, + { + 'line': 'https:*.@kitename:localhost:80:@kitesecret', + 'params': {'protocol': 'https', + 'kitename': '*.@kitename', + 'backend_port': '80', + 'backend_host': 'localhost', + 'secret': '@kitesecret'} + }, + { + 'line': 'raw/22:@kitename:localhost:22:@kitesecret', + 'params': {'protocol': 'raw/22', + 'kitename': '@kitename', + 'backend_port': '22', + 'backend_host': 'localhost', + 'secret': '@kitesecret'} + }, + ] + + def test_get_augeas_servicefile_path(self): + """ Test the generation of augeas-paths for pagekite services """ + tests = (('http', '80_http.rc'), + ('https', '443_https.rc'), + ('http/80', '80_http.rc'), + ('http/8080', '8080_http.rc'), + ('raw/22', '22_raw.rc')) + for protocol, filename in tests: + expected_path = os.path.join(CONF_PATH, filename, 'service_on') + returned_path = get_augeas_servicefile_path(protocol) + self.assertEqual(expected_path, returned_path) + + with self.assertRaises(ValueError): + get_augeas_servicefile_path('xmpp') + + def test_deconstruct_params(self): + """ Test deconstructing parameter dictionaries into strings """ + for test in self._tests: + self.assertEqual(test['line'], deconstruct_params(test['params'])) + + def test_construct_params(self): + """ Test constructing parameter dictionaries out of string """ + for test in self._tests: + self.assertEqual(test['params'], construct_params(test['line'])) + + line = "'https/80'; touch /etc/fstab':*.@kitename:localhost:80:foo'" + with self.assertRaises(RuntimeError): + construct_params(line) From 8955e41f15f5523b6ffd1d450a22fe99f3ed6297 Mon Sep 17 00:00:00 2001 From: fonfon Date: Tue, 20 Jan 2015 17:18:53 +0000 Subject: [PATCH 03/21] pagekite is required to be installed via package now --- plinth/modules/pagekite/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index 370944b03..f94286473 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -20,6 +20,7 @@ from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse, reverse_lazy from django.http.response import HttpResponseRedirect from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator from django.views.generic import View, TemplateView from django.views.generic.edit import FormView @@ -46,7 +47,10 @@ def index(request): class ContextMixin(object): - """Mixin to add 'subsubmenu' and 'title' to the context.""" + """Mixin to add 'subsubmenu' and 'title' to the context. + + Also requires 'pagekite' to be installed. + """ def get_context_data(self, **kwargs): """Use self.title and the module-level subsubmenu""" context = super(ContextMixin, self).get_context_data(**kwargs) @@ -54,8 +58,12 @@ class ContextMixin(object): context['subsubmenu'] = subsubmenu return context + @method_decorator(package.required('pagekite')) + def dispatch(self, *args, **kwargs): + return super(ContextMixin, self).dispatch(*args, **kwargs) -class DeleteServiceView(View): + +class DeleteServiceView(ContextMixin, View): def post(self, request): form = CustomServiceForm(request.POST) if form.is_valid(): From c04897be8bc672528c41bd9733874b406a3e2716 Mon Sep 17 00:00:00 2001 From: fonfon Date: Wed, 21 Jan 2015 12:59:47 +0000 Subject: [PATCH 04/21] Moved pagekit url creation to pagekite module plus some minor cleanup --- actions/pagekite | 4 +- .../templates/pagekite_custom_services.html | 12 ++--- .../pagekite/templatetags/pagekite_extras.py | 44 +++++++++++++++++++ plinth/modules/pagekite/util.py | 28 ++++++------ plinth/modules/pagekite/views.py | 7 +-- plinth/templatetags/plinth_extras.py | 19 -------- 6 files changed, 70 insertions(+), 44 deletions(-) create mode 100644 plinth/modules/pagekite/templatetags/pagekite_extras.py diff --git a/actions/pagekite b/actions/pagekite index 34bbaadaa..3891802d7 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -223,10 +223,10 @@ def subcommand_add_service(arguments): root = get_new_service_path(params['protocol']) # TODO: after adding a service, augeas fails writing the config; - # so do it manually here + # so add the service_on entry manually instead path = convert_augeas_path_to_filepath(root) with open(path, 'a') as servicefile: - line = "service_on = %s" % deconstruct_params(params) + line = "\nservice_on = %s\n" % deconstruct_params(params) servicefile.write(line) diff --git a/plinth/modules/pagekite/templates/pagekite_custom_services.html b/plinth/modules/pagekite/templates/pagekite_custom_services.html index d865d9d40..ee0ff9ca3 100644 --- a/plinth/modules/pagekite/templates/pagekite_custom_services.html +++ b/plinth/modules/pagekite/templates/pagekite_custom_services.html @@ -20,7 +20,7 @@ {% load bootstrap %} -{% load plinth_extras %} +{% load pagekite_extras %} {% block page_head %} {% endblock %} {% block content %} -

Custom Services

-
+
+
+

Create a custom service

+ {{ form|bootstrap_horizontal:'col-lg-6' }} + {% csrf_token %} +
+
+ +
+
+
+
+ +

Existing custom services

{% if not custom_services %} You don't have any Custom Services enabled @@ -65,13 +76,15 @@ {% else %} {{ service_url }} {% endif %} +
+ connected to {{ service.backend_host }}:{{ service.backend_port }}
{% csrf_token %} - {{ service.form }} + {{ service.form.as_p }}
-
- -

Create a custom service

- {{ form|bootstrap_horizontal:'col-lg-6' }} - {% csrf_token %} - - -
-
{% endblock %} diff --git a/plinth/modules/pagekite/templates/pagekite_default_services.html b/plinth/modules/pagekite/templates/pagekite_default_services.html index 0e0db1603..f9654efe8 100644 --- a/plinth/modules/pagekite/templates/pagekite_default_services.html +++ b/plinth/modules/pagekite/templates/pagekite_default_services.html @@ -38,12 +38,10 @@ {% block content %} -

Default Services

-
diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py index 64118076e..9fac5b859 100644 --- a/plinth/modules/pagekite/util.py +++ b/plinth/modules/pagekite/util.py @@ -28,7 +28,11 @@ LOGGER = logging.getLogger(__name__) KITE_NAME = '@kitename' KITE_SECRET = '@kitesecret' BACKEND_HOST = 'localhost' -# predefined services show up in the PredefinedServiceForm as checkbox + +# Predefined services are used to build the PredefinedServiceForm +# +# ATTENTION: When changing the params, make sure that the AddCustomServiceForm +# still recognizes when you try to add a service equal to a predefined one PREDEFINED_SERVICES = { 'http': { 'params': {'protocol': 'http', diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index 319800c8c..adfb0bcb5 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -26,7 +26,9 @@ from django.views.generic.edit import FormView from plinth import package from .util import get_pagekite_config, get_pagekite_services, \ get_kite_details, prepare_service_for_display -from .forms import ConfigurationForm, DefaultServiceForm, CustomServiceForm +from .forms import ConfigurationForm, DefaultServiceForm, \ + AddCustomServiceForm, DeleteCustomServiceForm + subsubmenu = [{'url': reverse_lazy('pagekite:index'), 'text': _('About PageKite')}, @@ -65,7 +67,7 @@ class ContextMixin(object): class DeleteServiceView(ContextMixin, View): def post(self, request): - form = CustomServiceForm(request.POST) + form = DeleteCustomServiceForm(request.POST) if form.is_valid(): form.delete(request) return HttpResponseRedirect(reverse('pagekite:custom-services')) @@ -79,7 +81,7 @@ class CustomServiceView(ContextMixin, TemplateView): **kwargs) unused, custom_services = get_pagekite_services() for service in custom_services: - service['form'] = CustomServiceForm(initial=service) + service['form'] = AddCustomServiceForm(initial=service) context['custom_services'] = [prepare_service_for_display(service) for service in custom_services] context.update(get_kite_details()) @@ -87,15 +89,15 @@ class CustomServiceView(ContextMixin, TemplateView): def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) - form = CustomServiceForm(prefix="custom") + form = AddCustomServiceForm(prefix="custom") context['form'] = form return self.render_to_response(context) def post(self, request): - form = CustomServiceForm(request.POST, prefix="custom") + form = AddCustomServiceForm(request.POST, prefix="custom") if form.is_valid(): form.save(request) - form = CustomServiceForm(prefix="custom") + form = AddCustomServiceForm(prefix="custom") context = self.get_context_data() context['form'] = form From ef08f40447320feb346359c5b169eb70ba21a3b5 Mon Sep 17 00:00:00 2001 From: fonfon Date: Fri, 23 Jan 2015 10:35:56 +0000 Subject: [PATCH 14/21] minor layout cleanup --- actions/__init__.py | 0 actions/pagekite | 2 +- actions/pagekite_util.py | 11 +- plinth/__main__.py | 3 - plinth/modules/pagekite/__init__.py | 3 +- plinth/modules/pagekite/forms.py | 5 +- plinth/modules/pagekite/pagekite.py | 183 ------------------ .../templates/pagekite_configure.html | 6 +- .../templates/pagekite_default_services.html | 4 - .../templates/pagekite_introduction.html | 9 +- plinth/modules/pagekite/util.py | 58 +++++- plinth/modules/pagekite/views.py | 6 +- plinth/tests/test_pagekite_actions.py | 7 +- 13 files changed, 81 insertions(+), 216 deletions(-) delete mode 100644 actions/__init__.py delete mode 100644 plinth/modules/pagekite/pagekite.py diff --git a/actions/__init__.py b/actions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/actions/pagekite b/actions/pagekite index 0a9024158..56b692a86 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -18,7 +18,7 @@ # """ -Configuration helper for Plint PageKite interface +Configuration helper for Plinth PageKite interface Unfortunately there is no python3 package for augeas yet """ diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py index b3568d43a..b96786fe4 100644 --- a/actions/pagekite_util.py +++ b/actions/pagekite_util.py @@ -19,11 +19,14 @@ """ Utilities for configuring PageKite. - -The variables/functions defined here are used by both the action script -and the plinth pagekite module. """ -# ATTENTION: This file has to be both python2 and python3 compatible +# TODO: +# Once python-augeas is available for python3 import the following things +# from plinth.modules.pagekite.util (instead of having a copy in here): +# +# SERVICE_PARAMS, convert_service_to_string, convert_to_service +# +# until then, this file is python2 and python3 compatible for the unittests import os import shlex diff --git a/plinth/__main__.py b/plinth/__main__.py index 0c4a253a1..7ceacca80 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -222,9 +222,6 @@ def configure_django(): 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'plinth.modules.first_boot.middleware.FirstBootMiddleware', ), - MESSAGE_TAGS = { - messages_constants.ERROR: 'danger' - }, ROOT_URLCONF='plinth.urls', SECURE_PROXY_SSL_HEADER=secure_proxy_ssl_header, SESSION_ENGINE='django.contrib.sessions.backends.file', diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py index b9ad5fb14..18fcccbcb 100644 --- a/plinth/modules/pagekite/__init__.py +++ b/plinth/modules/pagekite/__init__.py @@ -21,9 +21,8 @@ Plinth module to configure PageKite from gettext import gettext as _ from plinth import cfg -from . import pagekite -__all__ = ['pagekite', 'init'] +__all__ = ['init'] depends = ['plinth.modules.apps'] diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index 2df7f00c8..1228b9d5e 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -23,10 +23,9 @@ from django import forms from django.contrib import messages from django.core import validators -from actions.pagekite_util import convert_service_to_string from plinth.errors import ActionError -from .util import PREDEFINED_SERVICES, _run, get_kite_details, KITE_NAME, \ - KITE_SECRET, BACKEND_HOST +from .util import _run, convert_service_to_string, get_kite_details, \ + BACKEND_HOST, KITE_NAME, KITE_SECRET, PREDEFINED_SERVICES LOGGER = logging.getLogger(__name__) diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py deleted file mode 100644 index a22c961d9..000000000 --- a/plinth/modules/pagekite/pagekite.py +++ /dev/null @@ -1,183 +0,0 @@ -# -# 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 -""" - - - - - - - - - - - - - return super(TrimmedCharField, self).clean(value) - - -class ConfigureForm(forms.Form): # pylint: disable-msg=W0232 - """Form to configure PageKite""" - enabled = forms.BooleanField(label=_('Enable PageKite'), - required=False) - - server = forms.CharField( - label=_('Server'), required=False, - help_text=_('Select your pagekite.net server. Set "pagekite.net" to ' - 'use the default pagekite.net server'), - widget=forms.TextInput()) - - kite_name = TrimmedCharField( - label=_('Kite name'), - help_text=_('Example: mybox1-myacc.pagekite.me'), - validators=[ - validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$', - _('Invalid kite name'))]) - - kite_secret = TrimmedCharField( - label=_('Kite secret'), - help_text=_('A secret associated with the kite or the default secret \ -for your account if no secret is set on the kite')) - - http_enabled = forms.BooleanField( - label=_('Web Server (HTTP)'), required=False, - help_text=_('Site will be available at \ -http://mybox1-myacc.pagekite.me \ -')) - - ssh_enabled = forms.BooleanField( - label=_('Secure Shell (SSH)'), required=False, - help_text=_('See SSH client setup instructions')) - - -@login_required -@package.required(['pagekite']) -def configure(request): - """Serve the configuration form""" - status = get_status() - - form = None - - if request.method == 'POST': - form = ConfigureForm(request.POST, prefix='pagekite') - # pylint: disable-msg=E1101 - if form.is_valid(): - _apply_changes(request, status, form.cleaned_data) - status = get_status() - form = ConfigureForm(initial=status, prefix='pagekite') - else: - form = ConfigureForm(initial=status, prefix='pagekite') - - return TemplateResponse(request, 'pagekite_configure.html', - {'title': _('Configure PageKite'), - 'status': status, - 'form': form, - 'subsubmenu': subsubmenu}) - - -def get_status(): - """ - Return the current status of PageKite configuration by - executing various actions. - """ - status = {} - - # PageKite service enabled/disabled - output = _run(['get-status']) - status['enabled'] = (output.split()[0] == 'enabled') - - # PageKite kite details - output = _run(['get-kite']) - kite_details = output.split() - status['kite_name'] = kite_details[0] - status['kite_secret'] = kite_details[1] - - # PageKite server: 'pagekite.net' if flag 'defaults' is set, - # the value of 'frontend' otherwise - use_pagekitenet_server = _run(['get-pagekitenet-frontend-status']) - if "enabled" in use_pagekitenet_server: - value = 'pagekite.net' - elif "disabled" in use_pagekitenet_server: - value = _run(['get-frontend']) - status['server'] = value.replace('\n', '') - - # Service status - status['service'] = {} - for service in ('http', 'ssh'): - output = _run(['get-service-status', service]) - status[service + '_enabled'] = (output.split()[0] == 'enabled') - - return status - - -def _apply_changes(request, old_status, new_status): - """Apply the changes to PageKite configuration""" - LOGGER.info('New status is - %s', new_status) - - if old_status != new_status: - _run(['stop']) - - if old_status['enabled'] != new_status['enabled']: - if new_status['enabled']: - _run(['set-status', 'enable']) - messages.success(request, _('PageKite enabled')) - else: - _run(['set-status', 'disable']) - messages.success(request, _('PageKite disabled')) - - if old_status['kite_name'] != new_status['kite_name'] or \ - old_status['kite_secret'] != new_status['kite_secret']: - _run(['set-kite', '--kite-name', new_status['kite_name'], - '--kite-secret', new_status['kite_secret']]) - messages.success(request, _('Kite details set')) - - if old_status['server'] != new_status['server']: - server = new_status['server'] - if server in ('defaults', 'default', 'pagekite.net'): - _run(['enable-pagekitenet-frontend']) - else: - _run(['set-frontend', server]) - messages.success(request, _('Pagekite server set')) - - for service in ['http', 'ssh']: - if old_status[service + '_enabled'] != \ - new_status[service + '_enabled']: - if new_status[service + '_enabled']: - _run(['set-service-status', service, 'enable']) - messages.success(request, _('Service enabled: {service}') - .format(service=service)) - else: - _run(['set-service-status', service, 'disable']) - messages.success(request, _('Service disabled: {service}') - .format(service=service)) - - if old_status != new_status: - _run(['start']) - - -def _run(arguments, superuser=True): - """Run a given command and raise exception if there was an error""" - command = 'pagekite-configure' - - if superuser: - return actions.superuser_run(command, arguments) - else: - return actions.run(command, arguments) diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html index 07c5c6066..8276a8994 100644 --- a/plinth/modules/pagekite/templates/pagekite_configure.html +++ b/plinth/modules/pagekite/templates/pagekite_configure.html @@ -27,7 +27,7 @@ {% include 'bootstrapform/field.html' with field=form.enabled %} -

PageKite Account

{{ form.server|bootstrap_horizontal }} @@ -47,9 +47,9 @@ $('#id_pagekite-enabled').change(function() { if ($('#id_pagekite-enabled').prop('checked')) { - $('.pagekite-post-enabled-form').show('slow'); + $('#pagekite-post-enabled-form').show('slow'); } else { - $('.pagekite-post-enabled-form').hide('slow'); + $('#pagekite-post-enabled-form').hide('slow'); } }); diff --git a/plinth/modules/pagekite/templates/pagekite_default_services.html b/plinth/modules/pagekite/templates/pagekite_default_services.html index f9654efe8..53fbd99e4 100644 --- a/plinth/modules/pagekite/templates/pagekite_default_services.html +++ b/plinth/modules/pagekite/templates/pagekite_default_services.html @@ -26,10 +26,6 @@ display: inline-block; margin: 0px 10px; } - div.custom-services span.service { - display: inline-block; - padding-top: 6px; - } input.btn { margin: 10px 15px; } diff --git a/plinth/modules/pagekite/templates/pagekite_introduction.html b/plinth/modules/pagekite/templates/pagekite_introduction.html index 23c7fe99c..2e3781b5c 100644 --- a/plinth/modules/pagekite/templates/pagekite_introduction.html +++ b/plinth/modules/pagekite/templates/pagekite_introduction.html @@ -22,7 +22,7 @@

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

    @@ -41,11 +41,10 @@ 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. You can use any -server that offers a pagekite service, for example +using a combination of tunnels and reverse proxies. You can use any +pagekite service provider, for example pagekite.net. -In future, it might be possible to use your buddy's +In future it might be possible to use your buddy's {{ cfg.box_name }} for this.

diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py index 9fac5b859..d6c52b7dd 100644 --- a/plinth/modules/pagekite/util.py +++ b/plinth/modules/pagekite/util.py @@ -17,17 +17,20 @@ from gettext import gettext as _ import logging +import shlex -from actions.pagekite_util import convert_to_service from plinth import actions LOGGER = logging.getLogger(__name__) # defaults for the credentials; @kitename acts as a placeholder and is # understood (and replaced with the actual kitename) by pagekite. +BACKEND_HOST = 'localhost' KITE_NAME = '@kitename' KITE_SECRET = '@kitesecret' -BACKEND_HOST = 'localhost' + +SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', + 'secret'] # Predefined services are used to build the PredefinedServiceForm # @@ -68,6 +71,57 @@ PREDEFINED_SERVICES = { } +def convert_to_service(service_string): + """ Convert a service string into a service parameter dictionary + >>> convert_to_service('https/443:@kitename:localhost:443:@kitesecret') + {'kitename': '@kitename', 'backend_host': 'localhost', \ +'secret': '@kitesecret', 'protocol': 'https/443', 'backend_port': '443'} + """ + # The actions.py uses shlex.quote() to escape/quote malicious user input. + # That affects '*.@kitename', so the params string gets quoted. + # If the string is escaped and contains '*.@kitename', look whether shlex + # would still quote/escape the string when we remove '*.@kitename'. + + if service_string.startswith("'") and service_string.endswith("'"): + unquoted_string = service_string[1:-1] + error_msg = "The parameters contain suspicious characters: %s " + if '*.@kitename' in service_string: + unquoted_test_string = unquoted_string.replace('*.@kitename', '') + if unquoted_test_string == shlex.quote(unquoted_test_string): + # no other malicious characters found, use the unquoted string + service_string = unquoted_string + else: + raise RuntimeError(error_msg % service_string) + else: + raise RuntimeError(error_msg % service_string) + + try: + params = dict(zip(SERVICE_PARAMS, service_string.split(':'))) + except Exception: + msg = """params are expected to be a ':'-separated string containing + values for: %s , for example:\n"--params + http/8000:@kitename:localhost:8000:@kitesecret" + """ + raise ValueError(msg % ", ".join(SERVICE_PARAMS)) + return params + + +def convert_service_to_string(service): + """ Convert service dict into a ":"-separated parameter string + + >>> convert_service_to_string({'kitename': '@kitename', \ +'backend_host': 'localhost', 'secret': '@kitesecret', \ +'protocol': 'https/443', 'backend_port': '443'}) + 'https/443:@kitename:localhost:443:@kitesecret' + """ + try: + service_string = ":".join([str(service[param]) for param in + SERVICE_PARAMS]) + except KeyError: + raise ValueError("Could not parse params: %s " % service) + return service_string + + def get_kite_details(): output = _run(['get-kite']) kite_details = output.split() diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index adfb0bcb5..8637b953f 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -30,6 +30,7 @@ from .forms import ConfigurationForm, DefaultServiceForm, \ AddCustomServiceForm, DeleteCustomServiceForm +required_packages = ('pagekite', 'augeas-tools', 'python-augeas') subsubmenu = [{'url': reverse_lazy('pagekite:index'), 'text': _('About PageKite')}, {'url': reverse_lazy('pagekite:configure'), @@ -50,7 +51,7 @@ def index(request): class ContextMixin(object): """Mixin to add 'subsubmenu' and 'title' to the context. - Also requires 'pagekite' to be installed. + Also adds the requirement of all necessary packages to be installed """ def get_context_data(self, **kwargs): """Use self.title and the module-level subsubmenu""" @@ -59,8 +60,7 @@ class ContextMixin(object): context['subsubmenu'] = subsubmenu return context - @method_decorator(package.required('pagekite', 'augeas-tools', - 'python-augeas')) + @method_decorator(package.required(required_packages)) def dispatch(self, *args, **kwargs): return super(ContextMixin, self).dispatch(*args, **kwargs) diff --git a/plinth/tests/test_pagekite_actions.py b/plinth/tests/test_pagekite_actions.py index 2c8d66b9e..c5904383e 100644 --- a/plinth/tests/test_pagekite_actions.py +++ b/plinth/tests/test_pagekite_actions.py @@ -18,12 +18,13 @@ import os import unittest -from actions.pagekite_util import get_augeas_servicefile_path, CONF_PATH, \ - convert_to_service, convert_service_to_string +from actions.pagekite_util import get_augeas_servicefile_path, CONF_PATH +from plinth.modules.pagekite.util import convert_to_service, \ + convert_service_to_string class TestPagekiteActions(unittest.TestCase): - # test-cases to convert parameter-strings into param dicts and back + """Test-cases for the pagekite action utils""" _tests = [ { 'line': 'https/8080:*.@kitename:localhost:8080:@kitesecret', From 7a4486fadfd17948bb0b2f6d843cb405d26922e6 Mon Sep 17 00:00:00 2001 From: fonfon Date: Sun, 3 May 2015 16:53:47 +0200 Subject: [PATCH 15/21] pagekite: restart service when config is changed so the changed config gets applied immediately, and not only after enabling/disabling the service manually. --- actions/pagekite | 7 +++++++ plinth/modules/pagekite/forms.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/actions/pagekite b/actions/pagekite index 56b692a86..e1a974c8e 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -54,6 +54,7 @@ def parse_arguments(): subparsers.add_parser('is-running', help='Get whether PakeKite is running') subparsers.add_parser('start-and-enable', help='Enable PageKite service') subparsers.add_parser('stop-and-disable', help='Disable PageKite service') + subparsers.add_parser('restart', help='Restart PageKite service') # Frontend subparsers.add_parser('get-frontend', help='Get pagekite frontend') @@ -95,6 +96,12 @@ def subcommand_is_running(_): print 'yes' if util.service_is_running('pagekite') else 'no' +def subcommand_restart(_): + """Restart the pagekite service""" + _service('restart') + print 'restarted' + + def subcommand_start_and_enable(_): aug.remove(PATHS['abort_not_configured']) aug.save() diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index 1228b9d5e..6407a2934 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -70,15 +70,18 @@ for your account if no secret is set on the kite')) if old != new: + config_changed = False if old['kite_name'] != new['kite_name'] or \ old['kite_secret'] != new['kite_secret']: _run(['set-kite', '--kite-name', new['kite_name'], '--kite-secret', new['kite_secret']]) messages.success(request, _('Kite details set')) + config_changed = True if old['server'] != new['server']: _run(['set-frontend', new['server']]) messages.success(request, _('Pagekite server set')) + config_changed = True if old['enabled'] != new['enabled']: if new['enabled']: @@ -88,6 +91,11 @@ for your account if no secret is set on the kite')) _run(['stop-and-disable']) messages.success(request, _('PageKite disabled')) + # Restart the service if the config was changed while the service + # was running, so the changes take effect. + elif config_changed and new['enabled']: + _run(['restart']) + class DefaultServiceForm(forms.Form): """Creates a form out of PREDEFINED_SERVICES""" From 7af92d9e652e20cd4b2a85616075b656f146bc04 Mon Sep 17 00:00:00 2001 From: fonfon Date: Sun, 3 May 2015 17:53:56 +0200 Subject: [PATCH 16/21] Updated unit- and doctests --- actions/pagekite_util.py | 31 ++++++++++++++++++- plinth/modules/__init__.py | 0 plinth/modules/pagekite/util.py | 16 ++++++++-- ...t_pagekite_actions.py => test_pagekite.py} | 18 +---------- 4 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 plinth/modules/__init__.py rename plinth/tests/{test_pagekite_actions.py => test_pagekite.py} (76%) diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py index b96786fe4..c41f5b052 100644 --- a/actions/pagekite_util.py +++ b/actions/pagekite_util.py @@ -95,7 +95,31 @@ def convert_service_to_string(service): def get_augeas_servicefile_path(protocol): - """Get the augeas path where a service for a protocol should be stored""" + """Get the augeas path where a service for a protocol should be stored + + TODO: Use doctests instead of unittests until we can use python3. + + >>> get_augeas_servicefile_path('http') + '/files/etc/pagekite.d/80_http.rc/service_on' + + >>> get_augeas_servicefile_path('https') + '/files/etc/pagekite.d/443_https.rc/service_on' + + >>> get_augeas_servicefile_path('http/80') + '/files/etc/pagekite.d/80_http.rc/service_on' + + >>> get_augeas_servicefile_path('http/8080') + '/files/etc/pagekite.d/8080_http.rc/service_on' + + >>> get_augeas_servicefile_path('raw/22') + '/files/etc/pagekite.d/22_raw.rc/service_on' + + >>> get_augeas_servicefile_path('xmpp') + Traceback (most recent call last): + ... + ValueError: Unsupported protocol: xmpp + + """ if not protocol.startswith(("http", "https", "raw")): raise ValueError('Unsupported protocol: %s' % protocol) @@ -112,3 +136,8 @@ def get_augeas_servicefile_path(protocol): relpath = '%s_%s.rc' % (port, _protocol) return os.path.join(CONF_PATH, relpath, 'service_on') + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plinth/modules/__init__.py b/plinth/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py index d6c52b7dd..d5d79983e 100644 --- a/plinth/modules/pagekite/util.py +++ b/plinth/modules/pagekite/util.py @@ -73,9 +73,14 @@ PREDEFINED_SERVICES = { def convert_to_service(service_string): """ Convert a service string into a service parameter dictionary - >>> convert_to_service('https/443:@kitename:localhost:443:@kitesecret') - {'kitename': '@kitename', 'backend_host': 'localhost', \ -'secret': '@kitesecret', 'protocol': 'https/443', 'backend_port': '443'} + >>> input = 'https/443:@kitename:localhost:443:@kitesecret' + >>> output = convert_to_service(input) + >>> expected_output = {'secret': '@kitesecret', + ... 'backend_host': 'localhost', 'kitename': '@kitename', + ... 'backend_port': '443', 'protocol': 'https/443'} + ... + >>> output == expected_output + True """ # The actions.py uses shlex.quote() to escape/quote malicious user input. # That affects '*.@kitename', so the params string gets quoted. @@ -193,3 +198,8 @@ def _run(arguments, superuser=True): return actions.superuser_run(command, arguments) else: return actions.run(command, arguments) + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plinth/tests/test_pagekite_actions.py b/plinth/tests/test_pagekite.py similarity index 76% rename from plinth/tests/test_pagekite_actions.py rename to plinth/tests/test_pagekite.py index c5904383e..2fc04f597 100644 --- a/plinth/tests/test_pagekite_actions.py +++ b/plinth/tests/test_pagekite.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # # This file is part of Plinth. # @@ -15,10 +16,8 @@ # along with this program. If not, see . # -import os import unittest -from actions.pagekite_util import get_augeas_servicefile_path, CONF_PATH from plinth.modules.pagekite.util import convert_to_service, \ convert_service_to_string @@ -50,21 +49,6 @@ class TestPagekiteActions(unittest.TestCase): }, ] - def test_get_augeas_servicefile_path(self): - """ Test the generation of augeas-paths for pagekite services """ - tests = (('http', '80_http.rc'), - ('https', '443_https.rc'), - ('http/80', '80_http.rc'), - ('http/8080', '8080_http.rc'), - ('raw/22', '22_raw.rc')) - for protocol, filename in tests: - expected_path = os.path.join(CONF_PATH, filename, 'service_on') - returned_path = get_augeas_servicefile_path(protocol) - self.assertEqual(expected_path, returned_path) - - with self.assertRaises(ValueError): - get_augeas_servicefile_path('xmpp') - def test_convert_service_to_string(self): """ Test deconstructing parameter dictionaries into strings """ for test in self._tests: From 4561c3bcd928488c3f353b1e1b906726d1e7f8f2 Mon Sep 17 00:00:00 2001 From: fonfon Date: Sun, 3 May 2015 19:19:15 +0200 Subject: [PATCH 17/21] Removed quote-checking functionality from pagekite actions.py doesn't use shlex.quote anymore so I don't have to check for accidentially quoted things anymore. --- actions/pagekite_util.py | 26 -------------------------- plinth/modules/pagekite/urls.py | 3 ++- plinth/modules/pagekite/util.py | 19 ------------------- plinth/tests/test_pagekite.py | 4 ---- 4 files changed, 2 insertions(+), 50 deletions(-) diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py index c41f5b052..db8f9ac1c 100644 --- a/actions/pagekite_util.py +++ b/actions/pagekite_util.py @@ -29,7 +29,6 @@ Utilities for configuring PageKite. # until then, this file is python2 and python3 compatible for the unittests import os -import shlex CONF_PATH = '/files/etc/pagekite.d' SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', @@ -42,31 +41,6 @@ def convert_to_service(service_string): {'kitename': '@kitename', 'backend_host': 'localhost', \ 'secret': '@kitesecret', 'protocol': 'https/443', 'backend_port': '443'} """ - # The actions.py uses shlex.quote() to escape/quote malicious user input. - # That affects '*.@kitename', so the params string gets quoted. - # If the string is escaped and contains '*.@kitename', look whether shlex - # would still quote/escape the string when we remove '*.@kitename'. - - # TODO: use shlex only once augeas-python supports python3 - if hasattr(shlex, 'quote'): - quotefunction = shlex.quote - else: - import pipes - quotefunction = pipes.quote - - if service_string.startswith("'") and service_string.endswith("'"): - unquoted_string = service_string[1:-1] - error_msg = "The parameters contain suspicious characters: %s " - if '*.@kitename' in service_string: - unquoted_test_string = unquoted_string.replace('*.@kitename', '') - if unquoted_test_string == quotefunction(unquoted_test_string): - # no other malicious characters found, use the unquoted string - service_string = unquoted_string - else: - raise RuntimeError(error_msg % service_string) - else: - raise RuntimeError(error_msg % service_string) - try: params = dict(zip(SERVICE_PARAMS, service_string.split(':'))) except Exception: diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py index 9f6a1921d..90b5ec3a7 100644 --- a/plinth/modules/pagekite/urls.py +++ b/plinth/modules/pagekite/urls.py @@ -36,5 +36,6 @@ urlpatterns = patterns( # pylint: disable-msg=C0103 url(r'^apps/pagekite/services/custom$', login_required(CustomServiceView.as_view()), name='custom-services'), url(r'^apps/pagekite/services/custom/delete$', - login_required(DeleteServiceView.as_view()), name='delete-custom-service'), + login_required(DeleteServiceView.as_view()), + name='delete-custom-service'), ) diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py index d5d79983e..16c809a8b 100644 --- a/plinth/modules/pagekite/util.py +++ b/plinth/modules/pagekite/util.py @@ -17,7 +17,6 @@ from gettext import gettext as _ import logging -import shlex from plinth import actions @@ -82,24 +81,6 @@ def convert_to_service(service_string): >>> output == expected_output True """ - # The actions.py uses shlex.quote() to escape/quote malicious user input. - # That affects '*.@kitename', so the params string gets quoted. - # If the string is escaped and contains '*.@kitename', look whether shlex - # would still quote/escape the string when we remove '*.@kitename'. - - if service_string.startswith("'") and service_string.endswith("'"): - unquoted_string = service_string[1:-1] - error_msg = "The parameters contain suspicious characters: %s " - if '*.@kitename' in service_string: - unquoted_test_string = unquoted_string.replace('*.@kitename', '') - if unquoted_test_string == shlex.quote(unquoted_test_string): - # no other malicious characters found, use the unquoted string - service_string = unquoted_string - else: - raise RuntimeError(error_msg % service_string) - else: - raise RuntimeError(error_msg % service_string) - try: params = dict(zip(SERVICE_PARAMS, service_string.split(':'))) except Exception: diff --git a/plinth/tests/test_pagekite.py b/plinth/tests/test_pagekite.py index 2fc04f597..a90e62e38 100644 --- a/plinth/tests/test_pagekite.py +++ b/plinth/tests/test_pagekite.py @@ -59,7 +59,3 @@ class TestPagekiteActions(unittest.TestCase): """ Test constructing parameter dictionaries out of string """ for test in self._tests: self.assertEqual(test['params'], convert_to_service(test['line'])) - - line = "'https/80'; touch /etc/fstab':*.@kitename:localhost:80:foo'" - with self.assertRaises(RuntimeError): - convert_to_service(line) From 0ffaaa3da775a27f8cab0362d278d58417f199f3 Mon Sep 17 00:00:00 2001 From: fonfon Date: Mon, 4 May 2015 10:20:37 +0200 Subject: [PATCH 18/21] Use JSON as pagekite action-script arguments This allows to safe some conversions --- actions/pagekite | 25 ++++++++-------- actions/pagekite_util.py | 38 +++++++++++------------- plinth/modules/pagekite/forms.py | 22 +++++++------- plinth/modules/pagekite/util.py | 51 ++++---------------------------- 4 files changed, 47 insertions(+), 89 deletions(-) diff --git a/actions/pagekite b/actions/pagekite index e1a974c8e..71d29e734 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -25,12 +25,13 @@ Unfortunately there is no python3 package for augeas yet import argparse import augeas +import json import os import subprocess import util -from pagekite_util import SERVICE_PARAMS, convert_to_service, \ - convert_service_to_string, get_augeas_servicefile_path, CONF_PATH +from pagekite_util import SERVICE_PARAMS, convert_service_to_string, \ + get_augeas_servicefile_path, load_service, CONF_PATH aug = augeas.Augeas() @@ -75,11 +76,10 @@ def parse_arguments(): subparsers.add_parser('get-services', help='Get list of enabled services') add_service = subparsers.add_parser('add-service', help='Add a pagekite service') - add_service.add_argument('--service', help='":"-separated service string') + add_service.add_argument('--service', help='json service dictionary') remove_service = subparsers.add_parser('remove-service', help='Remove a pagekite service') - remove_service.add_argument('--service', help='":"-separated service \ - string') + remove_service.add_argument('--service', help='json service dictionary') return parser.parse_args() @@ -150,17 +150,18 @@ def enable_pagekitenet_frontend(): def subcommand_get_services(arguments): """ lists all available (enabled) services """ for match in aug.match(PATHS['service_on']): - print ":".join([aug.get(os.path.join(match, param)) for param in - SERVICE_PARAMS]) + service = dict([(param, aug.get(os.path.join(match, param))) + for param in SERVICE_PARAMS]) + print json.dumps(service) def subcommand_remove_service(arguments): """Searches and removes the service(s) that match all given parameters""" - service = convert_to_service(arguments.service) + service = load_service(arguments.service) paths = get_existing_service_paths(service) # TODO: theoretically, everything to do here is: # [aug.remove(path) for path in paths] - # but augeas won't let you save the changed files, and won't tell you why + # but augeas won't let you save the changed files and doesn't say why for path in paths: filepath = convert_augeas_path_to_filepath(path) service_found = False @@ -185,14 +186,14 @@ def get_existing_service_paths(service): # construct an augeas query path with patterns like: # */service_on/*[protocol='http'] path = PATHS['service_on'] - for key, value in service.items(): - path += "[%s='%s']" % (key, value) + for param, value in service.items(): + path += "[%s='%s']" % (param, value) return aug.match(path) def subcommand_add_service(arguments): """Add one service""" - service = convert_to_service(arguments.service) + service = load_service(arguments.service) if get_existing_service_paths(service): msg = "Service with the parameters %s already exists" raise RuntimeError(msg % service) diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py index db8f9ac1c..26b8c757f 100644 --- a/actions/pagekite_util.py +++ b/actions/pagekite_util.py @@ -24,34 +24,20 @@ Utilities for configuring PageKite. # Once python-augeas is available for python3 import the following things # from plinth.modules.pagekite.util (instead of having a copy in here): # -# SERVICE_PARAMS, convert_service_to_string, convert_to_service +# SERVICE_PARAMS, convert_service_to_string # # until then, this file is python2 and python3 compatible for the unittests import os +import json + CONF_PATH = '/files/etc/pagekite.d' +# parameters that get stored in configuration service_on entries SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', 'secret'] -def convert_to_service(service_string): - """ Convert a service string into a service parameter dictionary - >>> convert_to_service('https/443:@kitename:localhost:443:@kitesecret') - {'kitename': '@kitename', 'backend_host': 'localhost', \ -'secret': '@kitesecret', 'protocol': 'https/443', 'backend_port': '443'} - """ - try: - params = dict(zip(SERVICE_PARAMS, service_string.split(':'))) - except Exception: - msg = """params are expected to be a ':'-separated string containing - values for: %s , for example:\n"--params - http/8000:@kitename:localhost:8000:@kitesecret" - """ - raise ValueError(msg % ", ".join(SERVICE_PARAMS)) - return params - - def convert_service_to_string(service): """ Convert service dict into a ":"-separated parameter string @@ -61,17 +47,27 @@ def convert_service_to_string(service): 'https/443:@kitename:localhost:443:@kitesecret' """ try: - service_string = ":".join([str(service[param]) for param in - SERVICE_PARAMS]) + service_string = ":".join([service[param] for param in SERVICE_PARAMS]) except KeyError: raise ValueError("Could not parse params: %s " % service) return service_string +def load_service(json_service): + """ create a service out of json command-line argument + + 1) parse json + 2) only use the parameters that we need (SERVICE_PARAMS) + 3) convert unicode to strings + """ + service = json.loads(json_service) + return dict((str(key), str(service[key])) for key in SERVICE_PARAMS) + + def get_augeas_servicefile_path(protocol): """Get the augeas path where a service for a protocol should be stored - TODO: Use doctests instead of unittests until we can use python3. + TODO: Once we use python3 switch from doctests to unittests >>> get_augeas_servicefile_path('http') '/files/etc/pagekite.d/80_http.rc/service_on' diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index 6407a2934..be998cf67 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -15,17 +15,18 @@ # along with this program. If not, see . # +import copy from gettext import gettext as _ +import json import logging -import copy from django import forms from django.contrib import messages from django.core import validators from plinth.errors import ActionError -from .util import _run, convert_service_to_string, get_kite_details, \ - BACKEND_HOST, KITE_NAME, KITE_SECRET, PREDEFINED_SERVICES +from .util import _run, get_kite_details, BACKEND_HOST, KITE_NAME, \ + KITE_SECRET, PREDEFINED_SERVICES LOGGER = logging.getLogger(__name__) @@ -71,6 +72,7 @@ for your account if no secret is set on the kite')) if old != new: config_changed = False + if old['kite_name'] != new['kite_name'] or \ old['kite_secret'] != new['kite_secret']: _run(['set-kite', '--kite-name', new['kite_name'], @@ -92,7 +94,7 @@ for your account if no secret is set on the kite')) messages.success(request, _('PageKite disabled')) # Restart the service if the config was changed while the service - # was running, so the changes take effect. + # was running, so changes take effect immediately. elif config_changed and new['enabled']: _run(['restart']) @@ -118,13 +120,13 @@ class DefaultServiceForm(forms.Form): for service_name in PREDEFINED_SERVICES.keys(): if self.initial[service_name] != formdata[service_name]: service = PREDEFINED_SERVICES[service_name]['params'] - service_string = convert_service_to_string(service) + service = json.dumps(service) if formdata[service_name]: - _run(['add-service', '--service', service_string]) + _run(['add-service', '--service', service]) messages.success(request, _('Service enabled: {name}') .format(name=service_name)) else: - _run(['remove-service', '--service', service_string]) + _run(['remove-service', '--service', service]) messages.success(request, _('Service disabled: {name}') .format(name=service_name)) @@ -170,8 +172,7 @@ class DeleteCustomServiceForm(BaseCustomServiceForm): def delete(self, request): service = self.convert_formdata_to_service(self.cleaned_data) - service_string = convert_service_to_string(service) - _run(['remove-service', '--service', service_string]) + _run(['remove-service', '--service', json.dumps(service)]) messages.success(request, _('Deleted custom service')) @@ -213,9 +214,8 @@ class AddCustomServiceForm(BaseCustomServiceForm): def save(self, request): service = self.convert_formdata_to_service(self.cleaned_data) - service_string = convert_service_to_string(service) try: - _run(['add-service', '--service', service_string]) + _run(['add-service', '--service', json.dumps(service)]) messages.success(request, _('Added custom service')) except ActionError as exception: if "already exists" in str(exception): diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py index 16c809a8b..9dd01c834 100644 --- a/plinth/modules/pagekite/util.py +++ b/plinth/modules/pagekite/util.py @@ -16,6 +16,7 @@ # from gettext import gettext as _ +import json import logging from plinth import actions @@ -70,44 +71,6 @@ PREDEFINED_SERVICES = { } -def convert_to_service(service_string): - """ Convert a service string into a service parameter dictionary - >>> input = 'https/443:@kitename:localhost:443:@kitesecret' - >>> output = convert_to_service(input) - >>> expected_output = {'secret': '@kitesecret', - ... 'backend_host': 'localhost', 'kitename': '@kitename', - ... 'backend_port': '443', 'protocol': 'https/443'} - ... - >>> output == expected_output - True - """ - try: - params = dict(zip(SERVICE_PARAMS, service_string.split(':'))) - except Exception: - msg = """params are expected to be a ':'-separated string containing - values for: %s , for example:\n"--params - http/8000:@kitename:localhost:8000:@kitesecret" - """ - raise ValueError(msg % ", ".join(SERVICE_PARAMS)) - return params - - -def convert_service_to_string(service): - """ Convert service dict into a ":"-separated parameter string - - >>> convert_service_to_string({'kitename': '@kitename', \ -'backend_host': 'localhost', 'secret': '@kitesecret', \ -'protocol': 'https/443', 'backend_port': '443'}) - 'https/443:@kitename:localhost:443:@kitesecret' - """ - try: - service_string = ":".join([str(service[param]) for param in - SERVICE_PARAMS]) - except KeyError: - raise ValueError("Could not parse params: %s " % service) - return service_string - - def get_kite_details(): output = _run(['get-kite']) kite_details = output.split() @@ -147,8 +110,11 @@ def get_pagekite_services(): # set all predefined services to 'disabled' by default [predefined.update({proto: False}) for proto in PREDEFINED_SERVICES.keys()] # now, search for the enabled ones - for serviceline in _run(['get-services']).split(): - service = convert_to_service(serviceline) + for serviceline in _run(['get-services']).split('\n'): + if not serviceline: # skip empty lines + continue + + service = json.loads(serviceline) for name, predefined_service in PREDEFINED_SERVICES.items(): if service == predefined_service['params']: predefined[name] = True @@ -179,8 +145,3 @@ def _run(arguments, superuser=True): return actions.superuser_run(command, arguments) else: return actions.run(command, arguments) - - -if __name__ == "__main__": - import doctest - doctest.testmod() From 702dbf5e612afdd517c80d8ab9e592bc745da320 Mon Sep 17 00:00:00 2001 From: fonfon Date: Mon, 4 May 2015 10:32:53 +0200 Subject: [PATCH 19/21] Renamed default services to standard services --- plinth/modules/pagekite/forms.py | 6 +++--- plinth/modules/pagekite/urls.py | 4 ++-- plinth/modules/pagekite/views.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index be998cf67..bd1c07843 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -99,12 +99,12 @@ for your account if no secret is set on the kite')) _run(['restart']) -class DefaultServiceForm(forms.Form): +class StandardServiceForm(forms.Form): """Creates a form out of PREDEFINED_SERVICES""" def __init__(self, *args, **kwargs): """Add the fields from PREDEFINED_SERVICES""" - super(DefaultServiceForm, self).__init__(*args, **kwargs) + super(StandardServiceForm, self).__init__(*args, **kwargs) kite = get_kite_details() for name, service in PREDEFINED_SERVICES.items(): if name in ('http', 'https'): @@ -208,7 +208,7 @@ class AddCustomServiceForm(BaseCustomServiceForm): is_predefined = False if is_predefined: msg = _("""This service is available as a default service. Please - use the 'Default Services' page to enable it.""") + use the 'Standard Services' page to enable it.""") raise forms.ValidationError(msg) return cleaned_data diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py index 90b5ec3a7..bed6835f6 100644 --- a/plinth/modules/pagekite/urls.py +++ b/plinth/modules/pagekite/urls.py @@ -22,7 +22,7 @@ URLs for the PageKite module from django.conf.urls import patterns, url from django.contrib.auth.decorators import login_required -from .views import DefaultServiceView, CustomServiceView, ConfigurationView, \ +from .views import StandardServiceView, CustomServiceView, ConfigurationView, \ DeleteServiceView, index @@ -32,7 +32,7 @@ urlpatterns = patterns( # pylint: disable-msg=C0103 url(r'^apps/pagekite/configure/$', login_required(ConfigurationView.as_view()), name='configure'), url(r'^apps/pagekite/services/default$', - login_required(DefaultServiceView.as_view()), name='default-services'), + login_required(StandardServiceView.as_view()), name='default-services'), url(r'^apps/pagekite/services/custom$', login_required(CustomServiceView.as_view()), name='custom-services'), url(r'^apps/pagekite/services/custom/delete$', diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index 8637b953f..4d347c29f 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -26,7 +26,7 @@ from django.views.generic.edit import FormView from plinth import package from .util import get_pagekite_config, get_pagekite_services, \ get_kite_details, prepare_service_for_display -from .forms import ConfigurationForm, DefaultServiceForm, \ +from .forms import ConfigurationForm, StandardServiceForm, \ AddCustomServiceForm, DeleteCustomServiceForm @@ -36,7 +36,7 @@ subsubmenu = [{'url': reverse_lazy('pagekite:index'), {'url': reverse_lazy('pagekite:configure'), 'text': _('Configure PageKite')}, {'url': reverse_lazy('pagekite:default-services'), - 'text': _('Default Services')}, + 'text': _('Standard Services')}, {'url': reverse_lazy('pagekite:custom-services'), 'text': _('Custom Services')}] @@ -105,10 +105,10 @@ class CustomServiceView(ContextMixin, TemplateView): return self.render_to_response(context) -class DefaultServiceView(ContextMixin, FormView): +class StandardServiceView(ContextMixin, FormView): template_name = 'pagekite_default_services.html' - title = 'PageKite Default Services' - form_class = DefaultServiceForm + title = 'PageKite Standard Services' + form_class = StandardServiceForm success_url = reverse_lazy('pagekite:default-services') def get_initial(self): @@ -116,7 +116,7 @@ class DefaultServiceView(ContextMixin, FormView): def form_valid(self, form): form.save(self.request) - return super(DefaultServiceView, self).form_valid(form) + return super(StandardServiceView, self).form_valid(form) class ConfigurationView(ContextMixin, FormView): From 5cca1f99cf21b8ad123bf8e843a5017f3ab888f7 Mon Sep 17 00:00:00 2001 From: fonfon Date: Mon, 4 May 2015 10:40:19 +0200 Subject: [PATCH 20/21] removed/skipped deprecated unit-test --- plinth/tests/test_pagekite.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plinth/tests/test_pagekite.py b/plinth/tests/test_pagekite.py index a90e62e38..7463f25f5 100644 --- a/plinth/tests/test_pagekite.py +++ b/plinth/tests/test_pagekite.py @@ -18,9 +18,6 @@ import unittest -from plinth.modules.pagekite.util import convert_to_service, \ - convert_service_to_string - class TestPagekiteActions(unittest.TestCase): """Test-cases for the pagekite action utils""" @@ -49,13 +46,10 @@ class TestPagekiteActions(unittest.TestCase): }, ] + @unittest.skip('Use this test once the function is in the pagekite module ' + 'instead of actions/pagekite_util.py') def test_convert_service_to_string(self): """ Test deconstructing parameter dictionaries into strings """ for test in self._tests: service_string = convert_service_to_string(test['params']) self.assertEqual(test['line'], service_string) - - def test_convert_to_service(self): - """ Test constructing parameter dictionaries out of string """ - for test in self._tests: - self.assertEqual(test['params'], convert_to_service(test['line'])) From 32e94058f683eff272649088da179567e0042c71 Mon Sep 17 00:00:00 2001 From: fonfon Date: Mon, 4 May 2015 11:04:35 +0200 Subject: [PATCH 21/21] More consistent renaming from default to standard services --- plinth/modules/pagekite/forms.py | 2 +- ...efault_services.html => pagekite_standard_services.html} | 0 plinth/modules/pagekite/urls.py | 5 +++-- plinth/modules/pagekite/views.py | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) rename plinth/modules/pagekite/templates/{pagekite_default_services.html => pagekite_standard_services.html} (100%) diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index bd1c07843..ea746f0eb 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -207,7 +207,7 @@ class AddCustomServiceForm(BaseCustomServiceForm): except KeyError: is_predefined = False if is_predefined: - msg = _("""This service is available as a default service. Please + msg = _("""This service is available as a standard service. Please use the 'Standard Services' page to enable it.""") raise forms.ValidationError(msg) return cleaned_data diff --git a/plinth/modules/pagekite/templates/pagekite_default_services.html b/plinth/modules/pagekite/templates/pagekite_standard_services.html similarity index 100% rename from plinth/modules/pagekite/templates/pagekite_default_services.html rename to plinth/modules/pagekite/templates/pagekite_standard_services.html diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py index bed6835f6..32c13548d 100644 --- a/plinth/modules/pagekite/urls.py +++ b/plinth/modules/pagekite/urls.py @@ -31,8 +31,9 @@ urlpatterns = patterns( # pylint: disable-msg=C0103 url(r'^apps/pagekite/$', login_required(index), name='index'), url(r'^apps/pagekite/configure/$', login_required(ConfigurationView.as_view()), name='configure'), - url(r'^apps/pagekite/services/default$', - login_required(StandardServiceView.as_view()), name='default-services'), + url(r'^apps/pagekite/services/standard$', + login_required(StandardServiceView.as_view()), + name='standard-services'), url(r'^apps/pagekite/services/custom$', login_required(CustomServiceView.as_view()), name='custom-services'), url(r'^apps/pagekite/services/custom/delete$', diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index 4d347c29f..185ed391d 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -35,7 +35,7 @@ subsubmenu = [{'url': reverse_lazy('pagekite:index'), 'text': _('About PageKite')}, {'url': reverse_lazy('pagekite:configure'), 'text': _('Configure PageKite')}, - {'url': reverse_lazy('pagekite:default-services'), + {'url': reverse_lazy('pagekite:standard-services'), 'text': _('Standard Services')}, {'url': reverse_lazy('pagekite:custom-services'), 'text': _('Custom Services')}] @@ -106,10 +106,10 @@ class CustomServiceView(ContextMixin, TemplateView): class StandardServiceView(ContextMixin, FormView): - template_name = 'pagekite_default_services.html' + template_name = 'pagekite_standard_services.html' title = 'PageKite Standard Services' form_class = StandardServiceForm - success_url = reverse_lazy('pagekite:default-services') + success_url = reverse_lazy('pagekite:standard-services') def get_initial(self): return get_pagekite_services()[0]