152 lines
7.0 KiB
Python
Executable File
152 lines
7.0 KiB
Python
Executable File
#!/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:]))
|