#!/usr/bin/python3 """Install Caddy, the router's HTTPS proxy, from the source https.caddy_source names. status shows the configured source, the source in effect and the installed and candidate versions of Caddy. apply installs Caddy when it is missing: from the Ubuntu archive, or with upstream from Caddy's own repository, which it configures first (vpnrouter.CADDY_APT_SOURCE, with the key in vpnrouter.CADDY_KEYRING). When upstream is configured and the repository is not, it configures the repository and replaces the installed Caddy with upstream's. It then restarts vpn-router-setup without waiting, which serves with the new Caddy; vpn-router-caddy.service runs it for vpn-router-setup. use-upstream sets https.caddy_source to upstream and applies it, printing each step. Once the repository is configured Caddy stays upstream's: apt keeps the higher upstream version, and ubuntu has no effect. A failure is a message on standard error and the exit status 1. -h or --help shows the commands. vpn-router-caddy [--json] status vpn-router-caddy apply vpn-router-caddy use-upstream """ import argparse import json import os import subprocess import sys import urllib.request sys.path.insert(0, '/usr/lib/vpn-router') import vpnrouter #: apt-get, waiting up to ten minutes for dpkg's lock, which a package installation that runs setup holds. APT = ['apt-get', '-o', 'DPkg::Lock::Timeout=600'] #: The options of apt-get install: no questions, and the configuration files of the installed Caddy kept. INSTALL = ['-y', '-o', 'Dpkg::Options::=--force-confdef', '-o', 'Dpkg::Options::=--force-confold', 'install'] #: Seconds the key of Caddy's repository may take to download. KEY_TIMEOUT = 60 class Parser(argparse.ArgumentParser): """An argument parser whose errors end with status 2.""" def error(self, message): print(f'{self.prog}: {message}; see {self.prog} --help', file=sys.stderr) sys.exit(2) def step(message): print(message, flush=True) def installed(): """The installed version of caddy, or None when it is not installed.""" result = subprocess.run(['dpkg-query', '-W', '-f=${Status}\t${Version}', 'caddy'], capture_output=True, text=True) status, _, version = result.stdout.partition('\t') return version if result.returncode == 0 and status == 'install ok installed' else None def candidate(): """The version of caddy apt would install, or None when it knows none.""" result = subprocess.run(['apt-cache', 'policy', 'caddy'], capture_output=True, text=True) for line in result.stdout.splitlines(): if line.strip().startswith('Candidate:'): value = line.split(':', 1)[1].strip() return None if value == '(none)' else value return None def effective(): """The source Caddy comes from: upstream once its repository is configured, ubuntu otherwise.""" return 'upstream' if vpnrouter.CADDY_APT_SOURCE.is_file() else 'ubuntu' def command(args): """Run a command with its output shown; SystemExit when it fails.""" step(f'$ {" ".join(args)}') result = subprocess.run(args, env=dict(os.environ, DEBIAN_FRONTEND='noninteractive')) if result.returncode != 0: sys.exit(f'vpn-router-caddy: {args[0]} failed with status {result.returncode}') def apply(): """Install Caddy, or replace it with upstream's, as https.caddy_source asks (see the module).""" source = vpnrouter.load_config()['https']['caddy_source'] repository = source == 'upstream' and not vpnrouter.CADDY_APT_SOURCE.is_file() if installed() and not repository: if source == 'ubuntu' and effective() == 'upstream': step("https.caddy_source is ubuntu, but Caddy's repository is configured, so Caddy stays upstream's.") else: step(f'Caddy {installed()} is installed from the {source} source.') return if repository: step(f"Downloading the key of Caddy's repository from {vpnrouter.CADDY_KEY_URL}") with urllib.request.urlopen(vpnrouter.CADDY_KEY_URL, timeout=KEY_TIMEOUT) as response: key = response.read() if not key.lstrip().startswith(b'-----BEGIN PGP PUBLIC KEY BLOCK-----'): sys.exit(f'vpn-router-caddy: {vpnrouter.CADDY_KEY_URL} did not answer with an OpenPGP public key') vpnrouter.CADDY_KEYRING.parent.mkdir(mode=0o755, parents=True, exist_ok=True) vpnrouter.CADDY_KEYRING.write_bytes(key) os.chmod(vpnrouter.CADDY_KEYRING, 0o644) vpnrouter.CADDY_APT_SOURCE.write_text( f'Types: deb\nURIs: {vpnrouter.CADDY_REPOSITORY}\nSuites: any-version\nComponents: main\n' f'Signed-By: {vpnrouter.CADDY_KEYRING}\n', encoding='utf-8') os.chmod(vpnrouter.CADDY_APT_SOURCE, 0o644) step(f"Caddy's repository is configured in {vpnrouter.CADDY_APT_SOURCE}.") command([*APT, 'update']) command([*APT, *INSTALL, 'caddy']) step(f'Caddy {installed()} is installed from the {effective()} source.') subprocess.run(['systemctl', 'restart', '--no-block', 'vpn-router-setup.service'], capture_output=True, text=True) step('vpn-router-setup restarted.') def main(argv): parser = Parser(prog='vpn-router-caddy', description='Install Caddy from the source https.caddy_source names.') parser.add_argument('--json', action='store_true', help='answer status in JSON') commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser) commands.add_parser('status', help='show the sources and the versions of Caddy') commands.add_parser('apply', help='install Caddy, or replace it with upstream\'s, as https.caddy_source asks') commands.add_parser('use-upstream', help="switch to Caddy's own repository and replace Caddy with upstream's") options = parser.parse_args(argv) try: if options.command == 'status': document = {'configured': vpnrouter.load_config()['https']['caddy_source'], 'source': effective(), 'installed': installed(), 'candidate': candidate()} if options.json: print(json.dumps(document, indent=2)) else: print(f'Configured source: {document["configured"]}\nSource in effect: {document["source"]}\n' f'Installed: {document["installed"] or "no"}\nCandidate: {document["candidate"] or "none"}') return 0 if options.command == 'use-upstream': with vpnrouter.config_lock(): data = vpnrouter.read_settings() current = data.get('https') data['https'] = {**(current if isinstance(current, dict) else {}), 'caddy_source': 'upstream'} vpnrouter.validate(vpnrouter.with_defaults(data)) vpnrouter.save_config(data) step('https.caddy_source is upstream.') apply() except vpnrouter.ConfigError as exc: print(f'vpn-router-caddy: {exc}', file=sys.stderr) return 1 except OSError as exc: print(f'vpn-router-caddy: {exc}', file=sys.stderr) return 1 return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))