#!/usr/bin/python3

import os
import shutil
import socket
import subprocess
import sys
import tempfile

if sys.version_info.major != 3 or sys.version_info.minor < 6:
    print("this script needs at least Python 3.7 to work", file=sys.stderr)
    sys.exit(1)


from collections import namedtuple
from pathlib import Path


def eprint(*args, **kwargs):
    kwargs['file'] = sys.stderr
    ex = kwargs.pop('exit', None)
    print(*args, **kwargs)

    if ex is not None:
        sys.exit(ex)


MountSpec = namedtuple(
    'MountSpec',
    'src dst rw fstype, mandatory'
)

# Python >= 3.7 support a 'defaults' kwargs in the Factory above, but I still
# want to support 3.6 which is the default on openSUSE :(
MountSpec.__new__.__defaults__ = (None, None, False, None, True)

QUILT_CONFIGS = (
    # per-user config
    os.path.expanduser("~/.quiltrc"),
    # system-wide config
    "/etc/quilt.quiltrc"
)

nsjail = shutil.which("nsjail")

if not nsjail:
    eprint("you need 'nsjail' (security/nsjail in OBS) for this wrapper to work", exit=1)

pkgroot = Path(os.getcwd())

# are we inside the already unpacked dir? If so mount the parent dir so we
# have access to the patches
if (pkgroot / "patches").is_symlink():
    pkgroot = pkgroot.parent

mounts = [
    MountSpec(src="/bin"),
    MountSpec(src="/lib"),
    MountSpec(src="/lib64"),
    MountSpec(src="/usr"),
    MountSpec(src="/sbin"),
    MountSpec(src="/etc/alternatives", mandatory=False),
    MountSpec(src="/etc/resolv.conf"),
    MountSpec(src="/etc/ld.so.cache"),
    MountSpec(src="/etc/rpm", mandatory=False),
    MountSpec(dst="/tmp", fstype="tmpfs", rw=True),
    MountSpec(dst="/var/tmp", fstype="tmpfs", rw=True),
    MountSpec(dst="/var/tmp", fstype="tmpfs", rw=True),
    MountSpec(src="/var/lib/rpm"),
    MountSpec(src=pkgroot, rw=True),
    MountSpec(dst=os.path.expanduser("~/rpmbuild"), fstype="tmpfs", rw=True),
]

# Autodetect nsswitch.conf
if os.path.isfile("/etc/nsswitch.conf"):
    mounts.append(MountSpec(src="/etc/nsswitch.conf"))
elif os.path.isfile("/usr/etc/nsswitch.conf"):
    mounts.append(MountSpec(src="/usr/etc/nsswitch.conf"))
else:
    eprint(f"nsswitch.conf not found (tried /etc and /usr/etc)", exit=2)

for config in QUILT_CONFIGS:
    if not Path(config).exists():
        continue

    spec = MountSpec(src=config)
    mounts.append(spec)

VERBOSE = "SQUILT_VERBOSE" in os.environ
ENTER_SHELL = "SQUILT_ENTER_SHELL" in os.environ
DEBUG = "SQUILT_DEBUG" in os.environ

if ENTER_SHELL:
    mounts.append(
        MountSpec(src="/dev", rw=True)
    )
else:
    mounts.extend([
        MountSpec(src="/dev/null", rw=True),
        MountSpec(src="/dev/urandom", rw=True),
    ])

extra_mounts = os.environ.get("SQUILT_EXTRA_MOUNTS", "")

for extra in extra_mounts.split(";"):
    extra = extra.strip()
    if not extra:
        continue
    spec = eval(extra)
    if type(spec) != MountSpec:
        eprint(f"expression {extra} in SQUILT_EXTRA_MOUNTS is not a MountSpec instance!", exit=2)

    mounts.append(spec)

config_settings = [
    ("name", '"quilt secure sandbox"'),
    ("hostname", f'"{socket.gethostname()}"'),
    ("description", '"This policy allows to run quilt in a secure way"'),
    ("cwd", f'"{os.getcwd()}"'),
    ("envar", f'"HOME={os.environ["HOME"]}"'),
    ("envar", f'"PATH={os.environ["PATH"]}"'),
]

for rlimit_class in ("as", "core", "cpu", "fsize", "nofile", "nproc", "stack"):
    config_settings.append((f"rlimit_{rlimit_class}_type", "HARD"))

if ENTER_SHELL:
    config_settings.extend([
        ("mount_proc", "true"),
        # this option is security relevant, because programs in the jail can put
        # back characters into our host environment's terminal
        ("skip_setsid", "true")
    ])
else:
    config_settings.append(
        ("time_limit", "120")
    )

# we need to create a temporary config file since mounts with
# : (like found in OBS package names) don't work on the command line
with tempfile.NamedTemporaryFile(mode='w') as tmpfile:

    for key, val in config_settings:
        tmpfile.write(f"{key}: {val}\n")

    for mnt in mounts:
        if mnt.src is None and mnt.fstype is None:
            eprint("Error in MountSpec ({str(mnt)}): neither src nor fstype", exit=1)
        elif mnt.fstype is not None and not mnt.rw:
            eprint("Error in MountSpec ({str(mnt)}): tmpfs w/o RW?", exit=1)
        elif mnt.src and not mnt.mandatory and not Path(mnt.src).exists():
            # check ourselves whether non-mandatory mounts exist to avoid ugly
            # warnings being output by nsjail
            continue

        tmpfile.write("mount {\n")
        if mnt.src is not None:
            tmpfile.write(f'\tsrc: "{mnt.src}"\n')

        dst = mnt.dst if mnt.dst is not None else mnt.src
        tmpfile.write(f'\tdst: "{dst}"\n')
        rw = "true" if mnt.rw else "false"
        tmpfile.write(f'\trw: {rw}\n')
        is_bind = "true"
        if mnt.src is None and mnt.fstype is not None:
            is_bind = "false"
        tmpfile.write(f'\tis_bind: {is_bind}\n')

        if mnt.fstype:
            tmpfile.write(f'\tfstype: "{mnt.fstype}"\n')

        if not mnt.mandatory:
            tmpfile.write('\tmandatory: false\n')
        tmpfile.write('}\n')

    tmpfile.flush()

    nsjail_opts = ["-Mo"]
    nsjail_target = []

    if not VERBOSE:
        nsjail_opts.append("-q")

    if ENTER_SHELL:
        print("Entering debug shell in target environment")
        nsjail_target = ["/bin/bash", "-i"]
    else:
        nsjail_target = ["/usr/bin/quilt"] + sys.argv[1:]

    nsjail_opts.extend(["--config", tmpfile.name, "--"])
    nsjail_cmdline = [nsjail] + nsjail_opts + nsjail_target

    if VERBOSE:
        print(' '.join(nsjail_cmdline))
    if DEBUG:
        with open(tmpfile.name, 'r') as cfgfd:
            print('configuration file content:')
            print('-' * 80)
            print(cfgfd.read())
            print('-' * 80)
    res = subprocess.call(nsjail_cmdline, shell=False)

sys.exit(res)
