#!/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 the ejabberd service """ import argparse import os import shutil import socket import stat import subprocess import sys import ruamel.yaml from plinth import action_utils from plinth.modules import config from plinth.modules.letsencrypt import LIVE_DIRECTORY as LE_LIVE_DIRECTORY EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump' EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump' EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem' def parse_arguments(): """Return parsed command line arguments as dictionary""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') # Preseed debconf values before packages are installed. pre_install = subparsers.add_parser( 'pre-install', help='Preseed debconf values before packages are installed.') pre_install.add_argument( '--domainname', help='The domain name that will be used by the XMPP service.') # Setup ejabberd configuration subparsers.add_parser('setup', help='Setup ejabberd configuration') subparsers.add_parser('enable', help='Enable XMPP service') subparsers.add_parser('disable', help='Disable XMPP service') # Prepare ejabberd for hostname change pre_hostname_change = subparsers.add_parser( 'pre-change-hostname', help='Prepare ejabberd for nodename change') pre_hostname_change.add_argument('--old-hostname', help='Previous hostname') pre_hostname_change.add_argument('--new-hostname', help='New hostname') # Update ejabberd nodename hostname_change = subparsers.add_parser('change-hostname', help='Update ejabberd nodename') hostname_change.add_argument('--old-hostname', help='Previous hostname') hostname_change.add_argument('--new-hostname', help='New hostname') # Update ejabberd with new domainname domainname_change = subparsers.add_parser( 'change-domainname', help='Update ejabberd with new domainname') domainname_change.add_argument('--domainname', help='New domainname') # Switch/check Message Archive Management (MAM) in ejabberd config help_MAM = 'Switch or check Message Archive Management (MAM).' mam = subparsers.add_parser('mam', help=help_MAM) mam.add_argument('command', choices=('enable', 'disable', 'status'), help=help_MAM) help_LE = "Add/drop Let's Encrypt certificate if configured domain matches" letsencrypt = subparsers.add_parser('letsencrypt', help=help_LE) letsencrypt.add_argument('command', choices=('add', 'drop'), help=help_LE) letsencrypt.add_argument('--domain', help='Domain name to drop.') subparsers.required = True return parser.parse_args() def subcommand_pre_install(arguments): """Preseed debconf values before packages are installed.""" domainname = arguments.domainname if not domainname: # If new domainname is blank, use hostname instead. domainname = socket.gethostname() subprocess.check_output( ['debconf-set-selections'], input=b'ejabberd ejabberd/hostname string ' + domainname.encode()) def subcommand_setup(_): """Enabled LDAP authentication""" with open(EJABBERD_CONFIG, 'r') as file_handle: conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True) for listen_port in conf['listen']: if 'tls' in listen_port: listen_port['tls'] = False conf['auth_method'] = 'ldap' conf['ldap_servers'] = [ruamel.yaml.scalarstring.DoubleQuotedScalarString( 'localhost')] conf['ldap_base'] = ruamel.yaml.scalarstring.DoubleQuotedScalarString( 'ou=users,dc=thisbox') with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) try: subprocess.check_output(['ejabberdctl', 'restart']) except subprocess.CalledProcessError as err: print('Failed to restart ejabberd with new configuration: %s', err) with action_utils.WebserverChange() as webserver_change: webserver_change.enable('jwchat-plinth') def subcommand_enable(_): """Enable XMPP service""" action_utils.service_enable('ejabberd') action_utils.webserver_enable('jwchat-plinth') def subcommand_disable(_): """Disable XMPP service""" action_utils.webserver_disable('jwchat-plinth') action_utils.service_disable('ejabberd') def subcommand_pre_change_hostname(arguments): """Prepare ejabberd for hostname change""" if not shutil.which('ejabberdctl'): print('ejabberdctl not found. Is ejabberd installed?') return old_hostname = arguments.old_hostname new_hostname = arguments.new_hostname subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP]) try: subprocess.check_output(['ejabberdctl', 'mnesia-change-nodename', 'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW]) os.remove(EJABBERD_BACKUP) except subprocess.CalledProcessError as err: print('Failed to change hostname in ejabberd backup database: %s', err) def subcommand_change_hostname(arguments): """Update ejabberd with new hostname""" if not shutil.which('ejabberdctl'): print('ejabberdctl not found. Is ejabberd installed?') return action_utils.service_stop('ejabberd') subprocess.call(['pkill', '-u', 'ejabberd']) # Make sure there aren't files in the Mnesia spool dir os.makedirs('/var/lib/ejabberd/oldfiles', exist_ok=True) subprocess.call('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/', shell=True) action_utils.service_start('ejabberd') # restore backup database if os.path.exists(EJABBERD_BACKUP_NEW): try: subprocess.check_output(['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW]) os.remove(EJABBERD_BACKUP_NEW) except subprocess.CalledProcessError as err: print('Failed to restore ejabberd backup database: %s', err) else: print('Could not load ejabberd backup database: %s not found' % EJABBERD_BACKUP_NEW) def subcommand_change_domainname(arguments): """Update ejabberd with new domainname""" if not shutil.which('ejabberdctl'): print('ejabberdctl not found. Is ejabberd installed?') return domainname = arguments.domainname if not domainname: # If new domainname is blank, use hostname instead. domainname = socket.gethostname() action_utils.service_stop('ejabberd') subprocess.call(['pkill', '-u', 'ejabberd']) # Add updated domainname to ejabberd hosts list. with open(EJABBERD_CONFIG, 'r') as file_handle: conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True) conf['hosts'].append(ruamel.yaml.scalarstring.DoubleQuotedScalarString( domainname)) with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) action_utils.service_start('ejabberd') def subcommand_mam(argument): """Enable, disable, or get status of Message Archive Management (MAM).""" with open(EJABBERD_CONFIG, 'r') as file_handle: conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True) if 'modules' not in conf: print('Found no "modules" entry in ejabberd configuration file.') return if argument.command == 'status': if 'mod_mam' in conf['modules']: print('enabled') return else: print('disabled') return if argument.command == 'enable': # Explicitly set the recommended / default settings for mod_mam, # see https://docs.ejabberd.im/admin/configuration/#mod-mam. settings_mod_mam = {'mod_mam': { 'iqdisc': 'one_queue', # discipline, recommended 'one_queue' 'db_type': 'mnesia', # default is 'mnesia' (w/o set default_db) 'default': 'never', # policy, default 'never' 'request_activates_archiving': False, # default False 'assume_mam_usage': False, # for non-ack'd msgs, default False 'cache_size': 1000, # default is 1000 items 'cache_life_time': 3600 # default is 3600 seconds = 1h }} conf['modules'].update(settings_mod_mam) elif argument.command == 'disable': # disable modules by erasing from config file if 'mod_mam' in conf['modules']: conf['modules'].pop('mod_mam') else: print("Unknown command: %s" % argument.command) return with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) if action_utils.service_is_running('ejabberd'): subprocess.call(['ejabberdctl', 'reload_config']) def subcommand_letsencrypt(arguments): """ Add/drop usage of Let's Encrypt cert. The command 'add' applies only to current domain, will be called by action 'letsencrypt run_renew_hooks', when certbot renews the cert (if ejabberd is selected for cert use). Drop of a cert must be possible for any domain to respond to domain change. """ current_domain = config.get_domainname() with open(EJABBERD_CONFIG, 'r') as file_handle: conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True) if arguments.domain is not None and arguments.domain not in conf['hosts']: print('Aborted: Current domain "%s" not configured for ejabberd.' % arguments.domain) sys.exit(1) if arguments.command == 'add' and arguments.domain is not None \ and arguments.domain != current_domain: print('Aborted: Only certificate of current domain "%s" can be added.' % current_domain) sys.exit(2) if arguments.domain is None: arguments.domain = current_domain cert_folder = '/etc/ejabberd/letsencrypt/' + arguments.domain cert_file = cert_folder + '/ejabberd.pem' if arguments.command == 'add': le_folder = os.path.join(LE_LIVE_DIRECTORY, current_domain) le_privkey = os.path.join(le_folder, 'privkey.pem') le_fullchain = os.path.join(le_folder, 'fullchain.pem') if not os.path.exists(le_folder): print('Aborted: No certificate directory at %s.' % le_folder) sys.exit(3) if not os.path.exists(cert_folder): os.makedirs(cert_folder) shutil.chown(cert_folder, 'ejabberd', 'ejabberd') with open(cert_file, 'w') as outfile: with open(le_privkey, 'r') as infile: for line in infile: if line.strip(): outfile.write(line) with open(le_fullchain, 'r') as infile: for line in infile: if line.strip(): outfile.write(line) shutil.chown(cert_file, 'ejabberd', 'ejabberd') os.chmod(cert_file, stat.S_IRUSR | stat.S_IWUSR) cert_file = ruamel.yaml.scalarstring.DoubleQuotedScalarString( cert_file) conf['s2s_certfile'] = cert_file for listen_port in conf['listen']: if 'certfile' in listen_port: listen_port['certfile'] = cert_file else: # arguments.command == 'drop' (ensured by parser) orig_cert_file = ruamel.yaml.scalarstring.DoubleQuotedScalarString( EJABBERD_ORIG_CERT) for listen_port in conf['listen']: if 'certfile' in listen_port \ and listen_port['certfile'] == cert_file: listen_port['certfile'] = orig_cert_file if conf['s2s_certfile'] == cert_file: conf['s2s_certfile'] = orig_cert_file if os.path.exists(cert_folder): shutil.rmtree(cert_folder) with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) if action_utils.service_is_running('ejabberd'): action_utils.service_restart('ejabberd') 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()