#!/usr/bin/python3

import sys
import os
import re
import argparse
import subprocess
from pathlib import Path

from gi.repository import Gio, GLib

# Don't let warpinator run as root
if os.getuid() == 0:
    print("Warpinator should not be run as root. Please run it in user mode.")
    sys.exit(1)

try:
    src = os.environ["WARPINATOR_PATH"]
except KeyError:
    print("WARPINATOR_PATH missing from environment, exiting")
    exit(1)

sys.path.insert(0, src)
warpinator_path = os.path.join(src, "warpinator.py")
GLib.setenv("PYTHONPATH", ";".join(sys.path), True)

mode_help="""
Modes:

Warpinator can isolate the incoming folder so that files being received will be unable
to 'escape' (for instance, using relative symbolic links).

landlock: only available with kernel >= 5.13, and only if enabled. Warpinator will run like normal,
but individual transfers will be locked inside the incoming folder. You can see if it's enabled on your
system by running `cat /sys/kernel/security/lsm | grep landlock`.

bubblewrap: requires the bwrap command. This is usually in a package called 'bubblewrap' but that may
vary. Warpinator will run in a sandbox (similar to Flatpaks). Only the incoming folder will be writable.
Other locations will be read-only, including the user's home. Changing the incoming folder location will
require a program restart. This mode is not available in the Flatpak version

legacy: Warpinator will run without any sort of protection, though some effort will still be made to
detect relative links.

Warpinator ordinarily will choose which mode to use automatically, depending on what's available. It
will always prefer landlock over bubblewrap, with legacy as a last resort.

Flatpak users: If you can't use Landlock, you'll have to manage hardening yourself by adjusting runtime
permissions. See https://github.com/linuxmint/warp/blob/master/README.md#folder-isolation for information
on how to accomplish this.

"""

parser = argparse.ArgumentParser(description="Send and Receive Files across the Network",
                                 formatter_class=argparse.RawDescriptionHelpFormatter, epilog=mode_help)
parser.add_argument("-d", "--debug", help="Print debugging information.",
                    action="store_true")
parser.add_argument("-a", "--autostart", help="Exit if (dconf) org.x.warpinator.preferences 'autostart' is false.",
                    action="store_true")
parser.add_argument("-m", "--mode", help="Specify a sandbox method instead of letting Warpinator decide.",
                    action="store", choices=("legacy", "bubblewrap", "landlock"))
args = parser.parse_args()

if args.debug:
    os.environ["WARPINATOR_DEBUG"] = "1"

import config

# A system-installed google_auth (or similar) can pre-seed sys.modules['google']
# via a .pth file, pointing its __path__ only at the system location and
# preventing our bundled google/__init__.py from running. Drop the pre-seeded
# entry so the normal import machinery picks up our bundled copy via sys.path.
if config.bundle_grpc:
    sys.modules.pop("google", None)

# Remove xapp from GTK3_MODULES if this is a flatpak.
if config.FLATPAK_BUILD:
    mods_str = os.environ.get("GTK3_MODULES")
    if mods_str is not None:
        mods = mods_str.split(":")
        if "xapp-gtk3-module" in mods:
            mods.remove("xapp-gtk3-module")
            os.environ["GTK3_MODULES"] = ":".join(mods)

# Secure mode enforcement
import prefs
enforcer = prefs.SecureModePrefsBlocker()

if args.autostart:
    if not prefs.get_should_autostart():
        sys.exit(0)
del sys.argv[1:]

###########################
# See what mode we'll run in

supported_modes = ["legacy"]

if not config.FLATPAK_BUILD:
    if GLib.find_program_in_path("bwrap"):
        supported_modes.insert(0, "bubblewrap")
    else:
        if args.debug:
            print("Bubblewrap (bwrap) not found")
try:
    import landlock
    landlock.landlock_abi_version()
    supported_modes.insert(0, "landlock")
except ModuleNotFoundError as e:
    if args.debug:
        print("Landlock support unavailable - landlock python module not found (https://github.com/Edward-Knight/landlock)")
except landlock.SyscallError as e:
    if args.debug:
        print("Landlock support unavailable: %s" % str(e))
except ValueError:
    pass

if args.mode:
    if args.mode in supported_modes:
        sandbox_mode = args.mode
    else:
        print("'%s' mode is not available, see `warpinator --help` for more information" % args.mode)
        sys.exit(1)
else:
    sandbox_mode = supported_modes[0]

os.environ["WARPINATOR_SANDBOX_MODE"] = sandbox_mode
############################

############################
# Run Warpinator

# Keep using this process for landlock or legacy modes.
if sandbox_mode in ("legacy, landlock"):
    if sandbox_mode == "landlock":
        print("Using landlock for incoming file isolation")
    else:
        print("Running without incoming file isolation")

    try:
        import warpinator
        sys.exit(warpinator.main())
    except Exception as e:
        print(e)
        sys.exit(1)
# Otherwise bubblewrap will be a new process.
else:
    print("Using bubblewrap for incoming file isolation. Write access for the application will be limited to the save directory only.")
    import misc

    try:
        rundir = os.environ["XDG_RUNTIME_DIR"]
    except KeyError:
        rundir = "/run/user/%d" % os.getuid()

    def path_from_bus_addr(addr):
        return re.search(r"unix:(?:path|abstract)=(\/.+?)(?:,|$)", addr).group(1)

    try:
        session_socket_addr = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION, None)
        if session_socket_addr is None:
            session_socket_addr = os.environ.get("DBUS_SESSION_BUS_ADDRESS")

        session_path = path_from_bus_addr(session_socket_addr)
    except Exception as e:
        print("Could not retrieve the session dbus address: %s" % e)
        sys.exit(1)

    try:
        conn = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        res = conn.call_sync("org.a11y.Bus",
                             "/org/a11y/bus",
                             "org.a11y.Bus",
                             "GetAddress",
                             None, None,
                             Gio.DBusCallFlags.NONE,
                             -1, None)

        if res is not None:
            a11y_socket_addr = res[0]
        else:
            a11y_socket_addr = os.environ.get("AT_SPI_BUS_ADDRESS")

        a11y_path = path_from_bus_addr(a11y_socket_addr)
    except Exception as e:
        print("Could not retrieve the AT_SPI bus address, accessibility features may not work: %s" % e)

    launch_args = []

    launch_args += ["/usr/bin/bwrap"]
    launch_args += ["--ro-bind",         "/",                                               "/"]
    launch_args += ["--dev",             "/dev"]
    launch_args += ["--bind",            rundir + "/dconf",                                 rundir + "/dconf"]
    launch_args += ["--bind-try",        rundir + "/gvfsd",                                 rundir + "/gvfsd"]
    launch_args += ["--tmpfs",           "/tmp"]
    launch_args += ["--ro-bind",         session_path,                                      session_path]
    launch_args += ["--ro-bind-try",     a11y_path,                                         a11y_path]
    # The following two items aren't necessary if org.freedesktop.FileManager1 works properly - in that case,
    # the file manager is launched outside of the sandbox. If that fails, and the file manager isn't already running,
    # it will spawn inside the sandbox and be restricted the same as Warpinator.
    # For thumbnails mainly - they're fairly small, and nemo complains if a cache isn't there and writable
    cache = GLib.get_user_cache_dir()
    launch_args += ["--bind-try",        cache,                                             cache]
    # caja requires ~/.config/caja be r/w or else it won't start.
    caja_conf = os.path.join(GLib.get_user_config_dir(), "caja")
    launch_args += ["--bind-try",        caja_conf,                                         caja_conf]
    # Use a placeholder for the save location. This will be resolved inside the run loop below.
    launch_args += ["--bind",            "#save_path#",                                     "#save_path#"]

    launch_args += ["--die-with-parent"]

    # launch_args += ["/bin/sh"]
    launch_args += ["/usr/bin/python3",      warpinator_path]

    ret = 0

    while True:
        # We wait to set the actual save path until inside the restart loop, so it can be updated
        # and warpinator re-launched.
        real_args = [arg.replace("#save_path#", prefs.get_save_path()) for arg in launch_args]
        if args.debug:
            print()
            print(" ".join(real_args[:-2]).replace("--", "\n--"))
            print(" ".join(real_args[-2:]))

        try:
            ret = subprocess.run(real_args).returncode

            # Special code for restarting so the save path can be updated..
            if ret != misc.EXIT_CODE_RESTART_BWRAP:
                break
        except subprocess.CalledProcessError as e:
            print("Error launching warpinator: %s", str(e))
            ret = e.returncode
            break
        except KeyboardInterrupt:
            break
    sys.exit(ret)

