#!/usr/bin/python3 -I
"""Provision the installed helper for the desktop user authenticated by polkit."""
import fcntl
import grp
import hashlib
import json
import os
from pathlib import Path
import pwd
import stat
import subprocess
import sys
import tempfile
import time

LIB = Path('/usr/lib/publicvpnlist')
CONFIG = Path('/etc/publicvpnlist/helper.json')


def run(*args):
    return subprocess.run(args, check=True, stdout=subprocess.DEVNULL,
                          stderr=subprocess.DEVNULL, timeout=30)


def trusted(path, directory=False):
    info = path.lstat()
    kind = stat.S_ISDIR if directory else stat.S_ISREG
    if not kind(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
        raise ValueError('Untrusted installed path')


def caller_uid(environ):
    value = environ.get('PKEXEC_UID', '')
    if not value.isdecimal() or int(value) < 1000:
        raise ValueError('Launch setup from the desktop application')
    account = pwd.getpwuid(int(value))
    if account.pw_shell in ('/usr/sbin/nologin', '/sbin/nologin', '/bin/false'):
        raise ValueError('A desktop account is required')
    return account.pw_uid


def core_account():
    try:
        account = pwd.getpwnam('pvl-core')
    except KeyError:
        used = {x.pw_uid for x in pwd.getpwall()} | {x.gr_gid for x in grp.getgrall()}
        uid = next((n for n in range(999, 599, -1) if n not in used), None)
        if uid is None:
            raise ValueError('No free dedicated system identity')
        try:
            grp.getgrnam('pvl-core')
        except KeyError:
            pass
        else:
            raise ValueError('Existing pvl-core group requires administrator review')
        run('/usr/sbin/groupadd', '--system', '--gid', str(uid), 'pvl-core')
        run('/usr/sbin/useradd', '--system', '--uid', str(uid), '--gid', str(uid), '--no-create-home', '--home-dir', '/nonexistent', '--shell', '/usr/sbin/nologin', 'pvl-core')
        account = pwd.getpwnam('pvl-core')
    if account.pw_uid == 0 or account.pw_uid != account.pw_gid or account.pw_shell not in ('/usr/sbin/nologin', '/sbin/nologin', '/bin/false'):
        raise ValueError('Existing core identity is not suitable')
    return account.pw_uid


def ensure_unprotected():
    result = subprocess.run(['/usr/sbin/nft', '-j', 'list', 'tables'], check=True,
                            capture_output=True, timeout=10)
    tables = json.loads(result.stdout).get('nftables')
    if not isinstance(tables, list):
        raise ValueError('Cannot establish protection state')
    if any(x.get('table', {}).get('family') == 'inet' and
           x.get('table', {}).get('name') == 'pvl_vpn' for x in tables):
        raise ValueError('Disconnect or restore internet access before setup')


def existing_configuration(path, uid):
    if not path.exists() and not path.is_symlink():
        return None
    trusted(path)
    if stat.S_IMODE(path.stat().st_mode) != 0o600:
        raise ValueError('Existing configuration permissions need review')
    old = json.loads(path.read_text())
    if old.get('user_uid') != uid:
        raise ValueError('Application is already configured for another desktop user')
    return old


def write_configuration(path, config):
    # A crash cannot leave a partially written helper configuration.
    fd, name = tempfile.mkstemp(prefix='.setup-', dir=path.parent)
    try:
        with os.fdopen(fd, 'w') as stream:
            json.dump(config, stream)
            stream.write('\n')
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(name, path)
    finally:
        if os.path.exists(name):
            os.unlink(name)


def main():
    if os.geteuid() != 0 or len(sys.argv) != 1:
        raise ValueError('Administrator authentication required')
    uid = caller_uid(os.environ)
    for path in (Path('/usr'), Path('/usr/lib'), LIB):
        trusted(path, directory=True)
    for name in ('client-trust.json', 'sing-box', 'core.sha256', 'pvld', 'pvlctl'):
        trusted(LIB / name)
    trust = json.loads((LIB / 'client-trust.json').read_text())
    if set(trust) != {'api_origin', 'api_addresses', 'signing_keys'}:
        raise ValueError('Invalid packaged public trust')
    digest = hashlib.sha256((LIB / 'sing-box').read_bytes()).hexdigest()
    if digest != (LIB / 'core.sha256').read_text().strip():
        raise ValueError('Installed core checksum mismatch')
    CONFIG.parent.mkdir(mode=0o700, exist_ok=True)
    trusted(CONFIG.parent, directory=True)
    lock = os.open(CONFIG.parent / '.setup.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
    with os.fdopen(lock, 'w') as stream:
        fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
        old = existing_configuration(CONFIG, uid)
        ensure_unprotected()
        core_uid = core_account()
        if core_uid == uid:
            raise ValueError('Core and desktop identities must differ')
        config = dict(trust, core_path=str(LIB / 'sing-box'), core_sha256=digest,
                      core_uid=core_uid, user_uid=uid)
        if old is not None and (old.get('core_uid') != core_uid or
                                old.get('core_path') != config['core_path']):
            raise ValueError('Existing core configuration needs review')
        # Stop first so a concurrent connect cannot re-arm protection during setup.
        # A stopped service intentionally retains any protection; never remove it here.
        run('/usr/bin/systemctl', 'stop', 'publicvpnlist.service')
        ensure_unprotected()
        if old != config:
            write_configuration(CONFIG, config)
        run(str(LIB / 'pvld'), 'check-config')
        run('/usr/bin/systemctl', 'daemon-reload')
        run('/usr/bin/systemctl', 'enable', '--now', 'publicvpnlist.service')
        for _ in range(10):
            try:
                run('/usr/sbin/runuser', '-u', pwd.getpwuid(uid).pw_name, '--',
                    str(LIB / 'pvlctl'), 'status')
                print('Setup complete. VPN is disconnected.')
                return
            except subprocess.CalledProcessError:
                time.sleep(1)
        raise ValueError('Helper did not become ready')


if __name__ == '__main__':
    try:
        main()
    except (ValueError, KeyError, OSError, subprocess.SubprocessError) as error:
        print(str(error) if isinstance(error, ValueError) else 'Setup could not complete', file=sys.stderr)
        sys.exit(1)
