#!/usr/bin/env python3
"""Interactive host IPsec setup, launched by the authenticated admin terminal."""
import fnmatch
import ipaddress
import os
from pathlib import Path
import re
import sys
import tempfile


def validate(name, peer, local, remote, secret):
    if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,63}', name):
        raise ValueError('Invalid connection name')
    if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9.-]{0,252}', peer):
        raise ValueError('Invalid remote public IP or hostname')
    local = str(ipaddress.IPv4Network(local, strict=True))
    remote = str(ipaddress.IPv4Network(remote, strict=True))
    if not secret or any(ord(c) < 32 or ord(c) == 127 for c in secret):
        raise ValueError('The shared secret must be nonempty and on one line')
    return local, remote


def regular(path):
    if path.is_symlink() or (path.exists() and not path.is_file()):
        raise ValueError('Expected a regular file: ' + str(path))


def included(path, target):
    regular(path)
    for line in path.read_text().splitlines():
        match = re.match(r'^\s*include\s+(\S+)\s*(?:#.*)?$', line)
        if match and fnmatch.fnmatch(str(target), match[1]):
            return True
    return False


def ensure_include(path, target, pattern):
    if included(path,target):return
    status=path.stat()
    if status.st_uid!=os.geteuid() or status.st_mode & 0o022:
        raise ValueError('Unsafe host IPsec file owner or mode: '+str(path))
    original=path.read_bytes()
    fd,backup=tempfile.mkstemp(prefix=path.name+'.mykos-before-',dir=str(path.parent))
    with os.fdopen(fd,'wb') as handle:handle.write(original)
    fd=os.open(str(path),os.O_WRONLY|os.O_APPEND|os.O_NOFOLLOW)
    with os.fdopen(fd,'w') as handle:
        handle.write(('\n' if original and not original.endswith(b'\n') else '')+'include '+str(pattern)+'\n')
        handle.flush();os.fsync(handle.fileno())


def save_connection(root, name, peer, local, remote, secret):
    local, remote = validate(name, peer, local, remote, secret)
    directory = root / 'ipsec.d'
    if directory.is_symlink() or not directory.is_dir():
        raise ValueError('Host /etc/ipsec.d must be an existing directory')
    connection = 'mykos-' + name
    config = directory / (connection + '.conn.conf')
    secrets = directory / (connection + '.secrets')
    for path in (config, secrets):
        regular(path)
        if path.exists():
            raise ValueError('Connection already exists: ' + name)
    # Creation has been confirmed. Preserve global content and add only missing
    # MYKOS include globs, with a private backup before each append.
    ensure_include(root/'ipsec.conf',config,directory/'mykos-*.conn.conf')
    ensure_include(root/'ipsec.secrets',secrets,directory/'mykos-*.secrets')
    escaped = secret.replace('\\', '\\\\').replace('"', '\\"')
    config_text = ('conn ' + connection + '\n'
                   '  ikev2=insist\n  authby=secret\n'
                   '  left=%defaultroute\n  leftid=192.168.222.0\n  leftsubnet=' + local + '\n'
                   '  right=' + peer + '\n  rightid=192.168.222.0\n  rightsubnet=' + remote + '\n'
                   '  ike=aes128-sha1-dh21\n  esp=aes128-sha1\n  auto=start\n  encapsulation=yes\n')
    # Exclusive creation prevents replacing an existing tunnel or its PSK.
    created = []
    try:
        for path, text in ((secrets, '192.168.222.0 192.168.222.0 : PSK "' + escaped + '"\n'), (config, config_text)):
            fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
            created.append(path)
            with os.fdopen(fd, 'w') as handle:
                handle.write(text)
    except Exception:
        for path in created:
            path.unlink()
        raise
    return connection


def main():
    if sys.argv[1:] == ['--save']:
        # The shared workflow sends the PSK privately on stdin, never argv.
        data=sys.stdin.buffer.read(16385)
        if len(data)>16384:raise ValueError('Host VPN input is too large')
        values=data.decode('utf-8').split('\0')
        if len(values)!=5 or values[-1]!='':raise ValueError('Invalid host VPN input')
        name,peer,network,secret=values[:-1]
        if not re.fullmatch(r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}',network):
            raise ValueError('Enter the remote IPv4 network without /24')
        print(save_connection(Path('/etc'),name,peer,'192.168.222.0/24',network+'/24',secret))
        return
    if len(sys.argv)!=1:raise ValueError('Unexpected host VPN arguments')
    os.chdir(Path(__file__).resolve().parent)
    os.execv('/usr/bin/bash',['/usr/bin/bash','./pos','--add-host-vpn'])


if __name__ == '__main__':
    try:main()
    except (ValueError,OSError) as error:raise SystemExit('Host VPN setup: '+str(error))
