ejabberd: Use Let's Encrypt certificate, also across renewals.

Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Johannes Keyser 2017-07-25 03:47:07 +02:00 committed by James Valleroy
parent 1e57441dec
commit f628eb2cc2
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
6 changed files with 377 additions and 42 deletions

View File

@ -25,15 +25,20 @@ import argparse
import os import os
import shutil import shutil
import socket import socket
import stat
import subprocess import subprocess
import sys
import ruamel.yaml import ruamel.yaml
from plinth import action_utils from plinth import action_utils
from plinth.modules.config import config
from plinth.modules.letsencrypt import LIVE_DIRECTORY as LE_LIVE_DIRECTORY
EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump' EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump' EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem'
def parse_arguments(): def parse_arguments():
@ -85,6 +90,11 @@ def parse_arguments():
choices=('enable', 'disable', 'status'), choices=('enable', 'disable', 'status'),
help=help_MAM) 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 subparsers.required = True
return parser.parse_args() return parser.parse_args()
@ -262,6 +272,90 @@ def subcommand_mam(argument):
action_utils.service_restart('ejabberd') action_utils.service_restart('ejabberd')
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(): def main():
"""Parse arguments and perform all duties""" """Parse arguments and perform all duties"""
arguments = parse_arguments() arguments = parse_arguments()

View File

@ -27,14 +27,15 @@ import shutil
import subprocess import subprocess
import sys import sys
import re import re
import configobj import configobj
from plinth import action_utils from plinth import action_utils
from plinth.errors import ActionError
from plinth.modules.config import config from plinth.modules.config import config
from plinth.modules import letsencrypt as le
TEST_MODE = False TEST_MODE = False
LIVE_DIRECTORY = '/etc/letsencrypt/live/'
RENEWAL_DIRECTORY = '/etc/letsencrypt/renewal/' RENEWAL_DIRECTORY = '/etc/letsencrypt/renewal/'
AUTHENTICATOR = 'webroot' AUTHENTICATOR = 'webroot'
WEB_ROOT_PATH = '/var/www/html' WEB_ROOT_PATH = '/var/www/html'
@ -104,6 +105,10 @@ def parse_arguments():
choices=('enable', 'disable', 'status')) choices=('enable', 'disable', 'status'))
manage_hook_parser.add_argument('--domain', manage_hook_parser.add_argument('--domain',
help='Domain for hook management command.') help='Domain for hook management command.')
help_module_arg = 'For enable: Also use LE cert with other Plinth modules.'
manage_hook_parser.add_argument('--modules', help=help_module_arg,
nargs='+', default=[],
choices=le.MODULES_WITH_HOOKS)
help_domain_arg = 'Domain name to run the hook scripts with.' help_domain_arg = 'Domain name to run the hook scripts with.'
help_module_arg = 'Include hooks from the provided module names.' help_module_arg = 'Include hooks from the provided module names.'
@ -111,19 +116,25 @@ def parse_arguments():
run_pre_hooks_parser = subparsers.add_parser('run_pre_hooks', run_pre_hooks_parser = subparsers.add_parser('run_pre_hooks',
help=help_pre_hooks) help=help_pre_hooks)
run_pre_hooks_parser.add_argument('--domain', help=help_domain_arg) run_pre_hooks_parser.add_argument('--domain', help=help_domain_arg)
run_pre_hooks_parser.add_argument('--modules', help=help_module_arg) run_pre_hooks_parser.add_argument('--modules', help=help_module_arg,
nargs='+', default=[],
choices=le.MODULES_WITH_HOOKS)
help_renew_hooks = 'Maintenance tasks after a cert is actually renewed.' help_renew_hooks = 'Maintenance tasks after a cert is actually renewed.'
run_renew_hooks_parser = subparsers.add_parser('run_renew_hooks', run_renew_hooks_parser = subparsers.add_parser('run_renew_hooks',
help=help_renew_hooks) help=help_renew_hooks)
run_renew_hooks_parser.add_argument('--domain', help=help_domain_arg) run_renew_hooks_parser.add_argument('--domain', help=help_domain_arg)
run_renew_hooks_parser.add_argument('--modules', help=help_module_arg) run_renew_hooks_parser.add_argument('--modules', help=help_module_arg,
nargs='+', default=[],
choices=le.MODULES_WITH_HOOKS)
help_post_hooks = 'Maintenance tasks after a cert is obtained or renewed.' help_post_hooks = 'Maintenance tasks after a cert is obtained or renewed.'
run_post_hooks_parser = subparsers.add_parser('run_post_hooks', run_post_hooks_parser = subparsers.add_parser('run_post_hooks',
help=help_post_hooks) help=help_post_hooks)
run_post_hooks_parser.add_argument('--domain', help=help_domain_arg) run_post_hooks_parser.add_argument('--domain', help=help_domain_arg)
run_post_hooks_parser.add_argument('--modules', help=help_module_arg) run_post_hooks_parser.add_argument('--modules', help=help_module_arg,
nargs='+', default=[],
choices=le.MODULES_WITH_HOOKS)
subparsers.required = True subparsers.required = True
return parser.parse_args() return parser.parse_args()
@ -131,7 +142,7 @@ def parse_arguments():
def get_certificate_expiry(domain): def get_certificate_expiry(domain):
"""Return the expiry date of a certificate.""" """Return the expiry date of a certificate."""
certificate_file = os.path.join(LIVE_DIRECTORY, domain, 'cert.pem') certificate_file = os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem')
output = subprocess.check_output(['openssl', 'x509', '-enddate', '-noout', output = subprocess.check_output(['openssl', 'x509', '-enddate', '-noout',
'-in', certificate_file]) '-in', certificate_file])
return output.decode().strip().split('=')[1] return output.decode().strip().split('=')[1]
@ -159,12 +170,12 @@ def get_status():
Should be run as root, otherwise might yield a wrong, empty answer. Should be run as root, otherwise might yield a wrong, empty answer.
""" """
try: try:
domains = os.listdir(LIVE_DIRECTORY) domains = os.listdir(le.LIVE_DIRECTORY)
except OSError: except OSError:
domains = [] domains = []
domains = [domain for domain in domains domains = [domain for domain in domains
if os.path.isdir(os.path.join(LIVE_DIRECTORY, domain))] if os.path.isdir(os.path.join(le.LIVE_DIRECTORY, domain))]
domain_status = {} domain_status = {}
for domain in domains: for domain in domains:
@ -189,7 +200,7 @@ def subcommand_revoke(arguments):
domain = arguments.domain domain = arguments.domain
command = ['certbot', 'revoke', '--domain', domain, '--cert-path', command = ['certbot', 'revoke', '--domain', domain, '--cert-path',
os.path.join(LIVE_DIRECTORY, domain, 'cert.pem')] os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem')]
if TEST_MODE: if TEST_MODE:
command.append('--staging') command.append('--staging')
@ -229,8 +240,8 @@ def subcommand_obtain(arguments):
def subcommand_manage_hooks(arguments): def subcommand_manage_hooks(arguments):
""" """
Enable/disable/show status of certbot's pre-, renew-, and post-hooks. Enable/disable/status of certbot's pre-, renew-, & post-hooks for renewal.
Command enable only edits current domain, RENEWAL_DIRECTORY/DOMAIN.config, Enable edits renewal config of current domain (RENEWAL_DIR/DOMAIN.config),
and creates a backup beforehand that is restored if disable is called. and creates a backup beforehand that is restored if disable is called.
Commands disable and status work without error on any domain string. Commands disable and status work without error on any domain string.
""" """
@ -279,7 +290,7 @@ def subcommand_manage_hooks(arguments):
call_post = script_path + ' run_post_hooks --domain ' + arguments.domain call_post = script_path + ' run_post_hooks --domain ' + arguments.domain
config_plinth = {'renewalparams': config_plinth = {'renewalparams':
{'authenticator': AUTHENTICATOR, {'authenticator': AUTHENTICATOR,
'webroot_path': [WEB_ROOT_PATH], # 'webroot_path': [WEB_ROOT_PATH], # removed by renew...
'webroot_map': {arguments.domain: WEB_ROOT_PATH}, 'webroot_map': {arguments.domain: WEB_ROOT_PATH},
'installer': 'None', 'installer': 'None',
'pre_hook': call_pre, 'pre_hook': call_pre,
@ -290,23 +301,46 @@ def subcommand_manage_hooks(arguments):
for line in config_certbot.initial_comment]) for line in config_certbot.initial_comment])
if arguments.command == 'status': if arguments.command == 'status':
# check for presence of expected minimal configuration
config_checks = [(entry in config_certbot['renewalparams']) and config_checks = [(entry in config_certbot['renewalparams']) and
(str(config_plinth['renewalparams'][entry]) in (str(config_plinth['renewalparams'][entry]) in
str(config_certbot['renewalparams'][entry])) str(config_certbot['renewalparams'][entry]))
for entry in config_plinth['renewalparams'].keys()] for entry in config_plinth['renewalparams'].keys()]
if all(config_checks):
print('enabled') if not all(config_checks):
else:
print('disabled') print('disabled')
sys.exit(0)
# is enabled; check for which selected modules (only for renew_hook)
cmd_str = config_certbot['renewalparams']['renew_hook']
module_list = []
for mod_str in le.MODULES_WITH_HOOKS:
mod_pattern = 'letsencrypt .*--modules .*%s.*' % mod_str
match = re.search(mod_pattern, cmd_str)
if match is not None:
module_list.append(mod_str)
if module_list != []:
print('enabled, with modules: ' + ', '.join(module_list))
else:
print('enabled, without modules')
elif arguments.command == 'enable': elif arguments.command == 'enable':
if not config_edited_by_plinth: if not config_edited_by_plinth:
shutil.copy(config_path, config_backup_path) shutil.copy(config_path, config_backup_path)
config_certbot.initial_comment.append(comment_plinth) config_certbot.initial_comment.append(comment_plinth)
if arguments.modules != []:
call_renew += ' --modules ' + ' '.join(arguments.modules)
config_plinth['renewalparams']['renew_hook'] = call_renew
config_certbot['renewalparams'].update(config_plinth['renewalparams']) config_certbot['renewalparams'].update(config_plinth['renewalparams'])
config_certbot.write() config_certbot.write()
print('enabled successfully')
if arguments.modules != []:
print('enabled, with modules: ' + ', '.join(arguments.modules))
else:
print('enabled, without modules')
elif arguments.command == 'disable': elif arguments.command == 'disable':
# if changed, restore from backup; refuse disabling if no backup exists # if changed, restore from backup; refuse disabling if no backup exists
@ -338,16 +372,19 @@ def subcommand_run_pre_hooks(arguments):
print('Aborted: Current domain is %s, but called for %s.' % print('Aborted: Current domain is %s, but called for %s.' %
(current_domain, arguments.domain)) (current_domain, arguments.domain))
sys.exit(2) sys.exit(2)
sys.exit(0) sys.exit(0)
def subcommand_run_renew_hooks(arguments): def subcommand_run_renew_hooks(arguments):
""" """
Execute all needed maintenance tasks when a cert is renewed. Execute ALL maintenance tasks when (just after) certbot renews a cert, i.e.
run all tasks of every app/plinth module that supports usage of LE certs.
If registered as certbot's renew-hook, this script gets ONLY executed when If registered as certbot's renew-hook, this script gets ONLY executed when
certbot successfully renewed a certificate; with Debian default config, certbot successfully renewed a certificate; with Debian default config,
this means it would run about once every 60 days (renewals get executed this means it would run about once every 60 days (renewals get executed
if a cert is <30 days before expiry, and current default is 90 days). if a cert is <30 days before expiry, and current default is 90 days).
Errors will be logged by certbot to /var/log/letsencrypt/letsencrypt.log.
""" """
# Require current domain, to avoid confusion (e.g. call from old cron job). # Require current domain, to avoid confusion (e.g. call from old cron job).
if not arguments.domain: if not arguments.domain:
@ -363,9 +400,66 @@ def subcommand_run_renew_hooks(arguments):
if action_utils.service_is_running('apache2'): if action_utils.service_is_running('apache2'):
action_utils.service_restart('apache2') action_utils.service_restart('apache2')
for module in arguments.modules:
_run_action(module, ['letsencrypt', 'add']) # OK if only 1 module
# TODO: If >1 modules, collect errors and raise just one in the end,
# for certbot to log ALL failed attempts, not just 1st fail.
# try:
# _run_action(module, ['letsencrypt', 'add'])
# except Exception as err:
# pass
sys.exit(0) sys.exit(0)
def _run_action(action, action_options=None):
"""
Run a specific action from another module, for the run_renew_hooks command.
This function is a simplified version of plinth/actions.py, to enable
somewhat safe calls of other actions from outside the Plinth process.
The comments about the action contracts refer to plinth/actions.py.
"""
if action_options is None:
action_options = []
# Contract 3A and 3B: don't call anything outside of the actions directory.
# Assume the current path is the actions directly.
script_path = os.path.realpath(__file__)
actions_dir, _ = os.path.split(script_path)
if os.sep in action:
raise ValueError('Action cannot contain: ' + os.sep)
cmd = os.path.join(actions_dir, action)
if not os.path.realpath(cmd).startswith(actions_dir):
raise ValueError('Action has to be in directory %s' % actions_dir)
# Contract 3C: interpret shell escape sequences as literal file names.
# Contract 3E: fail if the action doesn't exist or exists elsewhere.
if not os.access(cmd, os.F_OK):
raise ValueError('Action must exist in action directory.')
cmd = [cmd]
# Contract: 3C, 3D: don't allow shell special characters in
# options be interpreted by the shell.
if action_options:
if not isinstance(action_options, (list, tuple)):
raise ValueError('Options must be list or tuple.')
cmd += list(action_options) # No escaping necessary
# Contract 3C: don't interpret shell escape sequences.
# Contract 5 (and 6-ish).
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=False)
output, error = proc.communicate()
output, error = output.decode(), error.decode()
if proc.returncode != 0:
raise ActionError(action, output, error)
def subcommand_run_post_hooks(arguments): def subcommand_run_post_hooks(arguments):
""" """
Execute all needed maintenance tasks AFTER a cert is obtained/renewed. Execute all needed maintenance tasks AFTER a cert is obtained/renewed.
@ -382,6 +476,7 @@ def subcommand_run_post_hooks(arguments):
print('Aborted: Current domain is %s, but called for %s.' % print('Aborted: Current domain is %s, but called for %s.' %
(current_domain, arguments.domain)) (current_domain, arguments.domain))
sys.exit(2) sys.exit(2)
sys.exit(0) sys.exit(0)

View File

@ -23,10 +23,12 @@ from django.utils.translation import ugettext_lazy as _
from plinth import actions from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth.errors import ActionError
from plinth.menu import main_menu from plinth.menu import main_menu
from plinth.modules import names from plinth.modules import names
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.signals import domainname_change from plinth.signals import domainname_change
from plinth import module_loader
version = 1 version = 1
@ -60,6 +62,9 @@ description = [
service = None service = None
MODULES_WITH_HOOKS = ['ejabberd']
LIVE_DIRECTORY = '/etc/letsencrypt/live/'
def init(): def init():
"""Intialize the module.""" """Intialize the module."""
@ -94,6 +99,29 @@ def on_domainname_change(sender, old_domainname, new_domainname, **kwargs):
del new_domainname # Unused del new_domainname # Unused
del kwargs # Unused del kwargs # Unused
for module in MODULES_WITH_HOOKS:
actions.superuser_run(module, ['letsencrypt', 'drop',
'--domain', old_domainname], async=True)
actions.superuser_run('letsencrypt', ['manage_hooks', 'disable', actions.superuser_run('letsencrypt', ['manage_hooks', 'disable',
'--domain', old_domainname], '--domain', old_domainname],
async=True) async=True)
def get_manage_hooks_status():
"""Return status of hook management for current domain."""
try:
output = actions.superuser_run('letsencrypt',
['manage_hooks', 'status'])
except ActionError:
return False
return output.strip()
def get_installed_modules():
installed_modules = [module_name for module_name, module in
module_loader.loaded_modules.items()
if module_name in MODULES_WITH_HOOKS
and module.setup_helper.get_state() == 'up-to-date']
return installed_modules

View File

@ -34,6 +34,9 @@
{% endblock %} {% endblock %}
{% block configuration %} {% block configuration %}
<h3>{% trans "Configuration" %}</h3>
{% if status.domains %} {% if status.domains %}
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
@ -134,6 +137,18 @@
</div> </div>
</div> </div>
{% include "diagnostics_button.html" with module="letsencrypt" enabled=True %}
<h4>{% trans "Certificate renewal management and use by other modules" %}</h4>
<p class="help-block">
{% blocktrans trimmed %}
If you have a Let's Encrypt certificate for your current domain, you may
let {{ box_name }} manage its renewal process. This also enables other apps
to use that certificate, so most users would not prompted with security
warnings when using them.
{% endblocktrans %}
</p>
<form class="form" method="post" <form class="form" method="post"
action="{% url 'letsencrypt:toggle_hooks' status.current_domain.name %}"> action="{% url 'letsencrypt:toggle_hooks' status.current_domain.name %}">
{% csrf_token %} {% csrf_token %}
@ -141,7 +156,9 @@
<label> <label>
{% if status.current_domain.name and status.current_domain.has_cert %} {% if status.current_domain.name and status.current_domain.has_cert %}
<input type="checkbox" name="toggle_hooks" id="id_toggle_hooks" <input type="checkbox" name="toggle_hooks" id="id_toggle_hooks"
{% if status.current_domain.manage_hooks_enabled %}checked{% endif %} {% if 'enabled' in status.current_domain.manage_hooks_status %}
checked
{% endif %}
onchange="this.form.submit();"> onchange="this.form.submit();">
</input> </input>
<noscript> <noscript>
@ -159,12 +176,12 @@
<span> <span>
{% if status.current_domain.name %} {% if status.current_domain.name %}
{% blocktrans with current_domain=status.current_domain.name %} {% blocktrans with current_domain=status.current_domain.name %}
Let Plinth manage certificate renewal of Let {{ box_name }} manage certificate renewal of
<b>{{ current_domain }}</b> (recommended) <b>{{ current_domain }}</b>
{% endblocktrans %} {% endblocktrans %}
{% else %} {% else %}
{% blocktrans with current_domain=status.current_domain.name %} {% blocktrans with current_domain=status.current_domain.name %}
Let Plinth manage certificate renewal of the current domain (recommended) Let {{ box_name }} manage certificate renewal of the current domain
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
</span> </span>
@ -173,14 +190,13 @@
<p class="help-block"> <p class="help-block">
{% if status.current_domain.name and status.current_domain.has_cert %} {% if status.current_domain.name and status.current_domain.has_cert %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
If {{ box_name }} manages the certificate renewal for the current domain, If enabled, {{ box_name }} can make sure that all apps can use the
it will ensure that all apps that use the certificate can use it, as soon certificate as soon as it is renewed.
as it gets renewed.
{% endblocktrans %} {% endblocktrans %}
{% elif not status.current_domain.has_cert %} {% elif not status.current_domain.has_cert %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>No certificate available for the current domain.</b> <b>No certificate available for the current domain.</b>
First obtain a certificate to enable management of its renewal. First obtain a certificate to enable its management.
{% endblocktrans %} {% endblocktrans %}
{% else %} {% else %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
@ -190,7 +206,66 @@
{% endif %} {% endif %}
</p> </p>
</form> </form>
{% include "diagnostics_button.html" with module="letsencrypt" enabled=True %}
<form class="form" method="post"
action="{% url 'letsencrypt:toggle_module' domain=status.current_domain.name module='ejabberd' %}">
{% csrf_token %}
<div class="checkbox">
<label>
{% if 'ejabberd' in installed_modules and 'enabled' in status.current_domain.manage_hooks_status %}
<input type="checkbox" name="ejabberd" id="ejabberd"
{% if 'ejabberd' in status.current_domain.manage_hooks_status %}
checked
{% endif %}
onchange="this.form.submit();">
</input>
<noscript>
<button class="btn btn-sm btn-default" type="submit">
{% trans "Update config" %}</button>
</noscript>
{% else %}
<input type="checkbox" name="ejabberd" id="ejabberd"
class="disabled"></input>
<noscript>
<button class="btn btn-sm btn-default disabled" type="submit">
{% trans "Update config" %}</button>
</noscript>
{% endif %}
<span>
{% if 'ejabberd' in installed_modules and 'enabled' in status.current_domain.manage_hooks_status %}
{% blocktrans with current_domain=status.current_domain.name %}
Use certificate of {{ current_domain }} for <b>ejabberd</b>
{% endblocktrans %}
{% else %}
{% blocktrans %}
Use certificate of the current domain for <b>ejabberd</b>
{% endblocktrans %}
{% endif %}
</span>
</label>
</div>
<p class="help-block">
{% url 'ejabberd:index' as ejabberd_url %}
{% if 'ejabberd' in installed_modules and 'enabled' in status.current_domain.manage_hooks_status %}
{% blocktrans trimmed %}
If enabled, the app <a href="{{ ejabberd_url }}">ejabberd</a> will also use the
Let's Encrypt certificate.
This will reduce warnings about self-signed certificates in client applications,
and enable more wide-spread federation with other XMPP servers in the Internet.
{% endblocktrans %}
{% elif 'ejabberd' not in installed_modules %}
{% blocktrans trimmed %}
This feature only makes sense if you are using the
<a href="{{ ejabberd_url }}">ejabberd</a> chat server app.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
To use a Let's Encrypt certificate for <a href="{{ ejabberd_url }}">ejabberd</a>
chat server app, you must first enable certificate renewal of the current domain.
{% endblocktrans %}
{% endif %}
</p>
</form>
{% else %} {% else %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
No domains have been configured. Configure domains to be able to No domains have been configured. Configure domains to be able to

View File

@ -33,4 +33,6 @@ urlpatterns = [
name='delete'), name='delete'),
url(r'^sys/letsencrypt/toggle_hooks/(?P<domain>[^/]+)/$', url(r'^sys/letsencrypt/toggle_hooks/(?P<domain>[^/]+)/$',
views.toggle_hooks, name='toggle_hooks'), views.toggle_hooks, name='toggle_hooks'),
url(r'^sys/letsencrypt/toggle_module/(?P<domain>[^/]+)/'
'(?P<module>[^/]+)/$', views.toggle_module, name='toggle_module'),
] ]

View File

@ -41,11 +41,12 @@ logger = logging.getLogger(__name__)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
return TemplateResponse(request, 'letsencrypt.html', return TemplateResponse(request, 'letsencrypt.html',
{'title': letsencrypt.name, {'title': letsencrypt.name,
'description': letsencrypt.description, 'description': letsencrypt.description,
'status': status}) 'status': status,
'installed_modules':
letsencrypt.get_installed_modules()})
@require_POST @require_POST
@ -102,8 +103,17 @@ def obtain(request, domain):
@require_POST @require_POST
def toggle_hooks(request, domain): def toggle_hooks(request, domain):
"""Toggle pointing of certbot's hooks to Plinth, for the current domain.""" """Toggle pointing of certbot's hooks to Plinth, for the current domain."""
subcommand = 'disable' if _hooks_manage_enabled() else 'enable' manage_hooks_status = letsencrypt.get_manage_hooks_status()
subcommand = 'disable' if 'enabled' in manage_hooks_status else 'enable'
try: try:
if subcommand == 'disable':
enabled_modules = [module for module in
letsencrypt.MODULES_WITH_HOOKS
if module in manage_hooks_status]
for module in enabled_modules:
actions.superuser_run(module, ['letsencrypt', 'drop'],
async=True)
actions.superuser_run('letsencrypt', ['manage_hooks', subcommand]) actions.superuser_run('letsencrypt', ['manage_hooks', subcommand])
if subcommand == 'enable': if subcommand == 'enable':
msg = _('Certificate renewal management enabled for {domain}.')\ msg = _('Certificate renewal management enabled for {domain}.')\
@ -122,10 +132,51 @@ def toggle_hooks(request, domain):
return redirect(reverse_lazy('letsencrypt:index')) return redirect(reverse_lazy('letsencrypt:index'))
@require_POST
def toggle_module(request, domain, module):
"""Toggle usage of LE cert for a module name, for the current domain."""
manage_hooks_status = letsencrypt.get_manage_hooks_status()
enabled_modules = [module for module in letsencrypt.MODULES_WITH_HOOKS
if module in manage_hooks_status]
if module in enabled_modules:
mod_le_arg = 'drop'
enabled_modules.remove(module)
else:
mod_le_arg = 'add'
enabled_modules.append(module)
module_args = ['letsencrypt', mod_le_arg]
le_arguments = ['manage_hooks', 'enable']
if not enabled_modules == []:
le_arguments.extend(['--modules', ' '.join(enabled_modules)])
try:
actions.superuser_run(module, module_args)
actions.superuser_run('letsencrypt', le_arguments)
messages.success(
request, _('Switched use of certificate for app {module}')
.format(module=module))
except ActionError as exception:
messages.error(
request,
_('Failed to switch certificate use for app {module}: {error}')
.format(module=module, error=exception.args[2]))
return redirect(reverse_lazy('letsencrypt:index'))
@require_POST @require_POST
def delete(request, domain): def delete(request, domain):
"""Delete a certificate for a given domain.""" """Delete a certificate for a given domain, and cleanup renewal config."""
try: # also delete any potential left-over renewal config backup manage_hooks_status = letsencrypt.get_manage_hooks_status()
enabled_modules = [module for module in letsencrypt.MODULES_WITH_HOOKS
if module in manage_hooks_status]
try:
for module in enabled_modules:
actions.superuser_run(module, ['letsencrypt', 'drop'], async=True)
actions.superuser_run('letsencrypt', ['manage_hooks', 'disable', actions.superuser_run('letsencrypt', ['manage_hooks', 'disable',
'--domain', domain]) '--domain', domain])
except ActionError as exception: except ActionError as exception:
@ -158,7 +209,7 @@ def get_status():
'name': curr_dom, 'name': curr_dom,
'has_cert': (curr_dom in status['domains'] and 'has_cert': (curr_dom in status['domains'] and
status['domains'][curr_dom]['certificate_available']), status['domains'][curr_dom]['certificate_available']),
'manage_hooks_enabled': _hooks_manage_enabled() 'manage_hooks_status': letsencrypt.get_manage_hooks_status()
} }
status['current_domain'] = current_domain status['current_domain'] = current_domain
@ -171,13 +222,3 @@ def get_status():
status['domains'].setdefault(domain, {}) status['domains'].setdefault(domain, {})
return status return status
def _hooks_manage_enabled():
"""Return status of hook management for current domain."""
try:
output = actions.superuser_run('letsencrypt',
['manage_hooks', 'status'])
except ActionError:
return False
return output.strip() == 'enabled'