#!/usr/bin/python3 # # 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 Let's Encrypt. """ import argparse import json import os import subprocess import sys from plinth import action_utils TEST_MODE = False LIVE_DIRECTORY = '/etc/letsencrypt/live/' APACHE_PREFIX = '/etc/apache2/sites-available/' APACHE_CONFIGURATION = ''' ServerAdmin webmaster@localhost ServerName {domain} DocumentRoot /var/www/html Options FollowSymLinks AllowOverride None Options Indexes FollowSymLinks MultiViews AllowOverride None Order allow,deny allow from all ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ AllowOverride None Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch Order allow,deny Allow from all ErrorLog ${{APACHE_LOG_DIR}}/error.log # Possible values include: debug, info, notice, warn, error, crit, alert, emerg. LogLevel warn CustomLog ${{APACHE_LOG_DIR}}/ssl_access.log combined # GnuTLS Switch: Enable/Disable SSL/TLS for this virtual host. GnuTLSEnable On # Automatically obtained certificates from Let's Encrypt GnuTLSCertificateFile /etc/letsencrypt/live/{domain}/fullchain.pem GnuTLSKeyFile /etc/letsencrypt/live/{domain}/privkey.pem # See http://www.outoforder.cc/projects/apache/mod_gnutls/docs/#GnuTLSPriorities GnuTLSPriorities NORMAL ''' def parse_arguments(): """Return parsed command line arguments as dictionary.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') subparsers.add_parser( 'get-status', help='Return the status of configured domains.') revoke_parser = subparsers.add_parser( 'revoke', help='Disable and domain and revoke its certificate.') revoke_parser.add_argument( '--domain', help='Domain name to revoke certificate for') obtain_parser = subparsers.add_parser( 'obtain', help='Obtain certificate for a domain and setup website.') obtain_parser.add_argument( '--domain', help='Domain name to obtain certificate for') return parser.parse_args() def get_certificate_expiry(domain): """Return the expiry date of a certificate.""" certificate_file = os.path.join(LIVE_DIRECTORY, domain, 'cert.pem') output = subprocess.check_output(['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file]) return output.decode().strip().split('=')[1] def subcommand_get_status(_): """Return a JSON dictionary of currently configured domains.""" try: domains = os.listdir(LIVE_DIRECTORY) except OSError: domains = [] domains = [domain for domain in domains if os.path.isdir(os.path.join(LIVE_DIRECTORY, domain))] domain_status = {} for domain in domains: domain_status[domain] = { 'certificate_available': True, 'expiry_date': get_certificate_expiry(domain), 'web_enabled': action_utils.webserver_is_enabled(domain, kind='site') } print(json.dumps({'domains': domain_status})) def subcommand_revoke(arguments): """Disable a domain and revoke the certificate.""" domain = arguments.domain command = ['letsencrypt', 'revoke', '--domain', domain, '--cert-path', os.path.join(LIVE_DIRECTORY, domain, 'cert.pem')] if TEST_MODE: command.append('--staging') process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() if process.returncode: print(stderr.decode(), file=sys.stderr) sys.exit(1) action_utils.webserver_disable(domain, kind='site') def subcommand_obtain(arguments): """Obtain a certificate for a domain and setup website.""" domain = arguments.domain command = [ 'letsencrypt', 'certonly', '--text', '--agree-tos', '--register-unsafely-without-email', '--domain', arguments.domain, '--authenticator', 'webroot', '--webroot-path', '/var/www/html/', '--renew-by-default'] if TEST_MODE: command.append('--staging') process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdin, stderr = process.communicate() if process.returncode: print(stderr.decode(), file=sys.stderr) sys.exit(1) setup_webserver_config(domain) action_utils.webserver_enable(domain, kind='site') def setup_webserver_config(domain): """Create SSL web server configuration for a domain. Do so only if there is no configuration existing. """ file_name = os.path.join(APACHE_PREFIX, domain + '.conf') if os.path.isfile(file_name): return with open(file_name, 'w') as file_handle: file_handle.write(APACHE_CONFIGURATION.format(domain=domain)) 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()