mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
ejabberd: Use Let's Encrypt certificate, also across renewals.
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
1e57441dec
commit
f628eb2cc2
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user