From 49b543599a06a15b1049e89ed20b5d0aa0da48ca Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 21 Jun 2019 11:11:45 -0700 Subject: [PATCH] ejabberd: Add let's encrypt component for managing certificates Signed-off-by: Sunil Mohan Adapa Reviewed-by: Joseph Nuthalapati --- actions/ejabberd | 111 ++++------------------------ plinth/modules/ejabberd/__init__.py | 37 +++++++++- plinth/modules/ejabberd/views.py | 2 +- 3 files changed, 51 insertions(+), 99 deletions(-) diff --git a/actions/ejabberd b/actions/ejabberd index 7700206d5..57bdfecb9 100755 --- a/actions/ejabberd +++ b/actions/ejabberd @@ -21,11 +21,10 @@ Configuration helper for the ejabberd service import argparse import os +import pathlib import shutil import socket -import stat import subprocess -import sys from distutils.version import LooseVersion as LV import ruamel.yaml @@ -56,7 +55,10 @@ def parse_arguments(): help='The domain name that will be used by the XMPP service.') # Setup ejabberd configuration - subparsers.add_parser('setup', help='Setup ejabberd configuration') + setup = subparsers.add_parser('setup', help='Setup ejabberd configuration') + setup.add_argument( + '--domainname', + help='The domain name that will be used by the XMPP service.') # Prepare ejabberd for hostname change pre_hostname_change = subparsers.add_parser( @@ -103,7 +105,7 @@ def subcommand_pre_install(arguments): input=b'ejabberd ejabberd/hostname string ' + domainname.encode()) -def subcommand_setup(_): +def subcommand_setup(arguments): """Enabled LDAP authentication""" with open(EJABBERD_CONFIG, 'r') as file_handle: conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True) @@ -122,7 +124,7 @@ def subcommand_setup(_): with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) - upgrade_config() + upgrade_config(arguments.domainname) try: subprocess.check_output(['ejabberdctl', 'restart']) @@ -130,7 +132,7 @@ def subcommand_setup(_): print('Failed to restart ejabberd with new configuration: %s', err) -def upgrade_config(): +def upgrade_config(domain): """Fix the config file by removing deprecated settings""" current_version = _get_version() if not current_version: @@ -154,6 +156,14 @@ def upgrade_config(): if listen_port['port'] == 5280: listen_port['port'] = 5443 + cert_dir = pathlib.Path('/etc/ejabberd/letsencrypt') / domain + cert_file = str(cert_dir / 'ejabberd.pem') + 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 + # Write changes back to the file with open(EJABBERD_CONFIG, 'w') as file_handle: ruamel.yaml.round_trip_dump(conf, file_handle) @@ -220,9 +230,6 @@ def subcommand_change_domainname(arguments): # 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) @@ -233,8 +240,6 @@ def subcommand_change_domainname(arguments): 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).""" @@ -284,90 +289,6 @@ def subcommand_mam(argument): 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 _get_version(): """ Get the current ejabberd version """ try: diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index 1852e9b89..1ac860288 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -19,6 +19,7 @@ FreedomBox app to configure ejabberd server. """ import logging +import pathlib from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -30,6 +31,7 @@ from plinth.daemon import Daemon from plinth.modules import config from plinth.modules.apache.components import Webserver from plinth.modules.firewall.components import Firewall +from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.signals import (domainname_change, post_hostname_change, pre_hostname_change) from plinth.utils import format_lazy @@ -42,6 +44,8 @@ managed_services = ['ejabberd'] managed_packages = ['ejabberd'] +managed_paths = [pathlib.Path('/etc/ejabberd/')] + name = _('ejabberd') short_description = _('Chat Server') @@ -104,6 +108,15 @@ class EjabberdApp(app_module.App): webserver = Webserver('webserver-ejabberd', 'jwchat-plinth') self.add(webserver) + letsencrypt = LetsEncrypt( + 'letsencrypt-ejabberd', domains=get_domains, daemons=['ejabberd'], + should_copy_certificates=True, + private_key_path='/etc/ejabberd/letsencrypt/{domain}/ejabberd.pem', + certificate_path='/etc/ejabberd/letsencrypt/{domain}/ejabberd.pem', + user_owner='ejabberd', group_owner='ejabberd', + managing_app='ejabberd') + self.add(letsencrypt) + daemon = Daemon('daemon-ejabberd', managed_services[0]) self.add(daemon) @@ -129,11 +142,29 @@ def setup(helper, old_version=None): helper.call('pre', actions.superuser_run, 'ejabberd', ['pre-install', '--domainname', domainname]) + # XXX: Configure all other domain names helper.install(managed_packages) - helper.call('post', actions.superuser_run, 'ejabberd', ['setup']) + helper.call('post', + app.get_component('letsencrypt-ejabberd').setup_certificates, + [domainname]) + helper.call('post', actions.superuser_run, 'ejabberd', + ['setup', '--domainname', domainname]) helper.call('post', app.enable) +def get_domains(): + """Return the list of domains that ejabberd is interested in. + + XXX: Retrieve the list from ejabberd configuration. + + """ + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() == 'needs-setup': + return [] + + return [config.get_domainname()] + + def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs): """ Backup ejabberd database before hostname is changed. @@ -165,8 +196,8 @@ def on_domainname_change(sender, old_domainname, new_domainname, **kwargs): del kwargs # Unused actions.superuser_run( - 'ejabberd', ['change-domainname', '--domainname', new_domainname], - run_in_background=True) + 'ejabberd', ['change-domainname', '--domainname', new_domainname]) + app.get_component('letsencrypt-ejabberd').setup_certificates() def diagnose(): diff --git a/plinth/modules/ejabberd/views.py b/plinth/modules/ejabberd/views.py index bb5d033d6..6e438fb87 100644 --- a/plinth/modules/ejabberd/views.py +++ b/plinth/modules/ejabberd/views.py @@ -47,7 +47,7 @@ class EjabberdAppView(AppView): def get_context_data(self, *args, **kwargs): """Add service to the context data.""" context = super().get_context_data(*args, **kwargs) - context['domainname'] = config.get_domainname() + context['domainname'] = ejabberd.get_domains()[0] context['clients'] = ejabberd.clients return context