# SPDX-License-Identifier: AGPL-3.0-or-later """Configure PageKite.""" import os from typing import Union import augeas from plinth import action_utils from plinth.actions import privileged from plinth.modules.pagekite import utils PATHS = { 'service_on': os.path.join(utils.CONF_PATH, '*', 'service_on', '*'), 'kitename': os.path.join(utils.CONF_PATH, '10_account.rc', 'kitename'), 'kitesecret': os.path.join(utils.CONF_PATH, '10_account.rc', 'kitesecret'), 'abort_not_configured': os.path.join(utils.CONF_PATH, '10_account.rc', 'abort_not_configured'), 'defaults': os.path.join(utils.CONF_PATH, '20_frontends.rc', 'defaults'), 'frontend': os.path.join(utils.CONF_PATH, '20_frontends.rc', 'frontend'), } @privileged def get_config() -> dict[str, object]: """Return the current configuration as JSON dictionary.""" aug = _augeas_load() if aug.match(PATHS['abort_not_configured']): aug.remove(PATHS['abort_not_configured']) aug.save() if aug.match(PATHS['defaults']): frontend = 'pagekite.net' else: frontend = aug.get(PATHS['frontend']) or '' frontend_parts = frontend.split(':') server_domain = frontend_parts[0] server_port = frontend_parts[1] if len(frontend_parts) >= 2 else '80' status = { 'kite_name': aug.get(PATHS['kitename']), 'kite_secret': aug.get(PATHS['kitesecret']), 'server_domain': server_domain, 'server_port': server_port, 'predefined_services': { proto: False for proto in utils.PREDEFINED_SERVICES }, 'custom_services': [], } # 1. predefined_services: {'http': False, 'ssh': True, 'https': True} # 2. custom_services: [{'protocol': 'http', 'secret' 'nono', ..}, [..]} for match in aug.match(PATHS['service_on']): service = dict([(param, aug.get(os.path.join(match, param))) for param in utils.SERVICE_PARAMS]) for name, predefined_service in utils.PREDEFINED_SERVICES.items(): if service == predefined_service['params']: status['predefined_services'][name] = True break else: status['custom_services'].append(service) if '/' in service['protocol']: service['protocol'], service['frontend_port'] = service[ 'protocol'].split('/') service['subdomains'] = service['kitename'].startswith('*.') kite_name = status['kite_name'] protocol = service['protocol'] if service['subdomains']: kite_name = f'*.{kite_name}' url = f'{protocol}://{kite_name}' if 'frontend_port' in service and service['frontend_port']: url = "%s:%s" % (url, service['frontend_port']) service['url'] = url return status @privileged def set_config(frontend: str, kite_name: str, kite_secret: str): """Set pagekite kite name, secret and frontend URL.""" aug = _augeas_load() aug.remove(PATHS['abort_not_configured']) aug.set(PATHS['kitename'], kite_name) aug.set(PATHS['kitesecret'], kite_secret) frontend_domain = frontend.split(':')[0] if frontend_domain in ('pagekite.net', 'defaults', 'default'): aug.set(PATHS['defaults'], '') aug.remove(PATHS['frontend']) else: aug.remove(PATHS['defaults']) aug.set(PATHS['frontend'], frontend) aug.save() for service_name in utils.PREDEFINED_SERVICES.keys(): service = utils.PREDEFINED_SERVICES[service_name]['params'] try: _add_service(aug, service) except RuntimeError: pass # Immediately after install, pagekite is enabled but not running. Restart # based on enabled state instead of try-restart. if action_utils.service_is_enabled('pagekite'): action_utils.service_restart('pagekite') @privileged def remove_service(service: dict[str, Union[str, bool]]): """Search and remove the service(s) that match all given parameters.""" aug = _augeas_load() service = utils.load_service(service) paths = _get_existing_service_paths(aug, 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 doesn't say why for path in paths: filepath = _convert_augeas_path_to_filepath(path) service_found = False with open(filepath, 'r', encoding='utf-8') as file: lines = file.readlines() for i, line in enumerate(lines): if line.startswith('service_on') and \ all(param in line for param in service.values()): lines[i] = "" service_found = True break if service_found: with open(filepath, 'w', encoding='utf-8') as file: file.writelines(lines) # abort to only allow deleting one service break action_utils.service_restart('pagekite') def _get_existing_service_paths(aug, service, keys=None): """Return paths of existing services that match the given service.""" # construct an augeas query path with patterns like: # */service_on/*[protocol='http'] path = PATHS['service_on'] for param in (keys or service.keys()): path += "[%s='%s']" % (param, service[param]) return aug.match(path) def _add_service(aug, service): """Add a new service into configuration.""" if _get_existing_service_paths(aug, service, ['protocol', 'kitename']): msg = "Service with the parameters %s already exists" raise RuntimeError(msg % service) root = _get_new_service_path(aug, service['protocol']) # TODO: after adding a service, augeas fails writing the config; # so add the service_on entry manually instead path = _convert_augeas_path_to_filepath(root) with open(path, 'a', encoding='utf-8') as servicefile: line = "\nservice_on = %s\n" % utils.convert_service_to_string(service) servicefile.write(line) @privileged def add_service(service: dict[str, Union[str, bool]]): """Add one service.""" aug = _augeas_load() service = utils.load_service(service) _add_service(aug, service) action_utils.service_try_restart('pagekite') 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(aug, protocol): """Get the augeas path of a new service for a protocol. This takes care of existing services using a /service_on/*/ query """ root = utils.get_augeas_servicefile_path(protocol) new_index = len(aug.match(root + '/*')) + 1 return os.path.join(root, str(new_index)) def _augeas_load(): """Initialize Augeas.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) aug.set('/augeas/load/Pagekite/lens', 'Pagekite.lns') aug.set('/augeas/load/Pagekite/incl[last() + 1]', '/etc/pagekite.d/*.rc') aug.load() return aug