#!/bin/bash
# incognito-session: written by Wren Linux parts/incognito.sh
# Runs as the user "incognito", or "wifi-signin" for the Wi-Fi sign-in window
# (the incognito command starts it through sudo; it is the only thing your
# user may run as either). Each user runs only its own kind of window: the
# sign-in one, whose firewall rule lets it reach the local network, never a
# normal one. Opens Firefox
# inside cage, a tiny Wayland compositor shown as one window in your sway.
# Home folder, profile, cache and downloads all live in /dev/shm (memory)
# and are deleted when Firefox closes.
# Sound: its own PipeWire (no sound card, no devices) whose only output is
# a pipe into your user's player; see the incognito command.
set -euo pipefail
umask 077
export PATH=/usr/local/bin:/usr/bin:/bin

die() { printf 'incognito: %s\n' "$*" >&2; exit 1; }

# Keeps it off XWayland's abstract socket, which any user can connect to
noabs=/usr/local/libexec/wren-no-abstract-sockets
[ -x "$noabs" ] || die "$noabs is missing. Run: wrenctl part incognito"

me=$(id -un)
[ "$me" = incognito ] || [ "$me" = wifi-signin ] || die "this runs as the user incognito or wifi-signin; use the incognito command."
# The firewall that keeps it off your computer (and, for incognito, your network),
# seen from this user: no rule, no window (bin/sandbox-net-check); and while it runs, it is watched
netcheck=/usr/local/libexec/wren-sandbox-net-check
[ -x "$netcheck" ] || die "$netcheck is missing. Run: wrenctl part incognito"
"$netcheck" || die "the firewall that keeps it off your network isn't active, so it wasn't started. Run: sudo systemctl restart wren-incognito-firewall"
[ $# -ge 5 ] || die "usage: incognito-session SOCKET_DIR WAYLAND_DISPLAY AUDIO THEME normal|signin [URL...]"
host_dir=$1 host_wl=$2 audio=$3 theme=$4 kind=$5
shift 5

# These come from the user who called us, so nothing else is accepted
[[ $host_dir =~ ^/dev/shm/wren-$me\.[A-Za-z0-9]+$ ]] || die "unexpected socket folder."
[[ $host_wl =~ ^[A-Za-z0-9._-]+$ ]] || die "unexpected Wayland display name."
[ -S "$host_dir/$host_wl" ] || die "can't reach the Wayland socket."
[[ $audio == - || $audio == incognito-audio.fifo ]] || die "unexpected sound pipe name."
[[ $theme == light || $theme == dark ]] || die "unexpected theme."
[[ $kind == normal || $kind == signin ]] || die "unexpected window kind."
{ [ "$me" = incognito ] && [ "$kind" = normal ]; } || { [ "$me" = wifi-signin ] && [ "$kind" = signin ]; } ||
    die "the user $me doesn't run the $kind window."
if [ "$audio" != - ]; then
    [ -p "$host_dir/$audio" ] || audio=-
    for c in pipewire wireplumber pipewire-pulse; do
        command -v "$c" >/dev/null || audio=-
    done
fi
for a in "$@"; do
    [[ $a =~ ^https?://[^[:space:]]+$ ]] || die "only http:// and https:// addresses can be opened."
done

[ -d /dev/shm ] || die "/dev/shm (memory) isn't available."
# Left behind by a crash or a power cut
find /dev/shm -maxdepth 1 -name "$me.*" -user "$me" -exec rm -rf {} + 2>/dev/null || true

base=$(mktemp -d "/dev/shm/$me.XXXXXXXX")
mkdir "$base/home" "$base/run" "$base/profile" "$base/tmp"
pid=

cleanup() {
    trap - EXIT INT TERM HUP
    if [ -n "$pid" ]; then
        kill -TERM -- "-$pid" 2>/dev/null || true
        sleep 1
        kill -KILL -- "-$pid" 2>/dev/null || true
    fi
    rm -rf "$base"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP

cp /usr/local/share/wren/incognito-user.js "$base/profile/user.js"
# Downloads go to memory too, and are gone when the window closes
{
    echo 'user_pref("browser.download.folderList", 2);'
    echo "user_pref(\"browser.download.dir\", \"$base/tmp\");"
    echo 'user_pref("browser.download.useDownloadDir", true);'
    # light or dark (bin/theme): Firefox's own look (websites are told light
    # anyway: fingerprinting resistance)
    if [ "$theme" = light ]; then
        echo 'user_pref("extensions.activeThemeID", "firefox-compact-light@mozilla.org");'
    else
        echo 'user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");'
    fi
} >>"$base/profile/user.js"

# doh_prefs normal|signin: Firefox's own DNS, set at every start. While secure
# DNS is on (parts/secure-dns.sh; /etc/resolv.conf says so) Firefox uses the
# system's, which is encrypted; while it is off, its own DNS over HTTPS to
# Quad9: strict (never plain DNS, so a hostile network can't push it back to
# plain DNS by blocking the encrypted kind) unless fallback was chosen at the
# sandboxed Firefox's install. The Wi-Fi sign-in window (signin) uses the
# network's own DNS: that is what it is for. (The same in firefox-session and
# incognito-session; a test checks.)
doh_prefs() {
    local mode=3
    if [ "$1" = signin ] ||
        { [ ! -L /etc/resolv.conf ] && grep -qsE '^# Written by Wren Linux (parts|extras)/secure-dns\.sh' /etc/resolv.conf; }; then
        mode=5
    elif [ "$(sed -n 's/^dns=//p' /etc/wren/firefox.conf 2>/dev/null | head -1)" = fallback ]; then
        mode=2
    fi
    echo "user_pref(\"network.trr.mode\", $mode);"
    echo 'user_pref("network.trr.uri", "https://dns.quad9.net/dns-query");'
    echo 'user_pref("network.trr.custom_uri", "https://dns.quad9.net/dns-query");'
}
doh_prefs "$kind" >>"$base/profile/user.js"
# The Wi-Fi sign-in window: sign-in pages are plain http (HTTPS-Only would
# stop at a warning first)
if [ "$kind" = signin ]; then
    echo 'user_pref("dom.security.https_only_mode", false);' >>"$base/profile/user.js"
fi

# Its own sound server. The only sink it has writes into the pipe; there
# are no sound cards and no microphone to reach. WirePlumber's "policy"
# profile does the linking without looking for any hardware.
if [ "$audio" != - ]; then
    mkdir -p "$base/home/.config/pipewire/pipewire.conf.d"
    cat >"$base/home/.config/pipewire/pipewire.conf.d/10-incognito.conf" <<EOF
context.modules = [
  { name = libpipewire-module-pipe-tunnel
    args = {
      tunnel.mode = sink
      pipe.filename = "$host_dir/$audio"
      audio.format = S16LE
      audio.rate = 48000
      audio.channels = 2
      audio.position = [ FL FR ]
      node.name = incognito-out
      node.description = "incognito"
    }
  }
]
EOF
fi

# cage connects to your sway (the restricted socket from wayland-restrict,
# by absolute WAYLAND_DISPLAY) and makes
# its own socket in XDG_RUNTIME_DIR for Firefox. Software rendering; the
# render group lets cage open the GPU's render node, which it needs to start. Everything below runs in one
# session, so closing it all is one signal to the group.
# shellcheck disable=SC2016  # expanded by the inner bash, on purpose
start='
    waitfor() { for _ in $(seq 50); do [ -S "$1" ] && return 0; sleep 0.1; done; return 1; }
    if [ -n "$INCOGNITO_AUDIO" ]; then
        pipewire &
        if waitfor "$XDG_RUNTIME_DIR/pipewire-0"; then
            wireplumber -p policy &
            pipewire-pulse &
            waitfor "$XDG_RUNTIME_DIR/pulse/native" || true
        fi
    fi
    exec cage -d -- /usr/lib/firefox-esr/firefox-esr --no-remote --profile "$INCOGNITO_PROFILE" "$@"'
setsid env -i \
    PATH="$PATH" LANG=C.UTF-8 \
    HOME="$base/home" TMPDIR="$base/tmp" XDG_RUNTIME_DIR="$base/run" \
    XDG_CONFIG_HOME="$base/home/.config" XDG_CACHE_HOME="$base/home/.cache" \
    XDG_DATA_HOME="$base/home/.local/share" \
    WAYLAND_DISPLAY="$host_dir/$host_wl" WLR_BACKENDS=wayland WLR_RENDERER=pixman \
    LIBGL_ALWAYS_SOFTWARE=1 MOZ_ENABLE_WAYLAND=1 MOZ_DBUS_REMOTE=0 GTK_USE_PORTAL=0 \
    DBUS_SESSION_BUS_ADDRESS=disabled: \
    PULSE_SERVER="unix:$base/run/pulse/native" \
    INCOGNITO_AUDIO="$([ "$audio" != - ] && echo 1)" INCOGNITO_PROFILE="$base/profile" \
    "$noabs" bash -c "$start" incognito-start "$@" &
pid=$!
"$netcheck" --watch "$pid" &
wait "$pid" || true
