762 lines
28 KiB
Bash
Executable File
762 lines
28 KiB
Bash
Executable File
#!/bin/sh
|
|
# provision-kiosk.sh — Linux kiosk provisioning wizard
|
|
#
|
|
# Turns a systemd Linux machine into a Vestibule kiosk without manual OS
|
|
# configuration:
|
|
#
|
|
# 1. Verify prerequisites: systemd, cage, a Gecko browser (LibreWolf
|
|
# or Firefox — native package, Flatpak, or snap), usher, python3
|
|
# 2. Create the dedicated kiosk user (password stays locked)
|
|
# 3. Stage files under /opt/vestibule (usher, extension XPI, configs)
|
|
# 4. Install /usr/local/bin/vestibule-kiosk-launch
|
|
# 5. Write /etc/vestibule/kiosk.env (home URL, browser + flavor)
|
|
# 6. Register the usher Native Messaging host for the kiosk user
|
|
# 7. Deploy a merged policies.json (policy force-installs the
|
|
# extension) where the chosen browser reads it
|
|
# 8. Generate + enable vestibule-kiosk.service (cage on a VT)
|
|
#
|
|
# The unit IS the graphical session: no display manager, no autologin
|
|
# config. cage takes the VT and runs the browser as the kiosk user,
|
|
# restarted by systemd if anything dies.
|
|
#
|
|
# Browser support (auto-detect picks the first available, LibreWolf
|
|
# preferred): librewolf native, librewolf flatpak, firefox native,
|
|
# firefox flatpak, firefox snap.
|
|
#
|
|
# Exit codes:
|
|
# 0 success
|
|
# 2 not root / not supported (no systemd)
|
|
# 3 prerequisite missing (cage, browser, usher, python3, dbus)
|
|
# 4 kiosk user error
|
|
# 5 unit install / enable failure
|
|
# 6 invalid parameters
|
|
#
|
|
# Unattended examples:
|
|
# sudo ./provision-kiosk.sh --home-url https://checkin.example.org \
|
|
# --kiosk-user kiosk --librewolf flatpak --yes --start
|
|
# sudo ./provision-kiosk.sh --firefox native \
|
|
# --home-url https://lobby.example.org --yes
|
|
#
|
|
# Interactive:
|
|
# sudo ./provision-kiosk.sh
|
|
#
|
|
# POSIX sh — no bashisms. Runs on any minimal Linux base.
|
|
|
|
set -eu
|
|
|
|
HOST_NAME="com.vestibule.usher"
|
|
EXT_ID="vestibule@vestibule.kiosk"
|
|
LW_FLATPAK_APP="io.gitlab.librewolf-community"
|
|
FF_FLATPAK_APP="org.mozilla.firefox"
|
|
OPT_ROOT="/opt/vestibule"
|
|
ENV_DIR="/etc/vestibule"
|
|
UNIT_SRC_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
PROJECT_ROOT=$(cd "${UNIT_SRC_DIR}/.." && pwd)
|
|
UNIT_DST="/etc/systemd/system/vestibule-kiosk.service"
|
|
LAUNCH_DST="/usr/local/bin/vestibule-kiosk-launch"
|
|
USHER_DST="/usr/local/bin/usher"
|
|
|
|
KIOSK_USER="vestibule-kiosk"
|
|
TTY_NUM="2"
|
|
BROWSER_REQ="auto"
|
|
FLAVOR_REQ="auto"
|
|
BROWSER_FLAGS_SEEN=""
|
|
HOME_URL="about:blank"
|
|
USHER_SRC=""
|
|
QUIET=0
|
|
CHECK=0
|
|
ASSUME_YES=0
|
|
START_NOW=0
|
|
|
|
usage() {
|
|
sed -n '2,48p' "$0" | sed 's/^# \{0,1\}//'
|
|
cat <<'EOF'
|
|
|
|
Options:
|
|
--home-url URL Kiosk home URL (default: about:blank)
|
|
--kiosk-user NAME Kiosk account name (default: vestibule-kiosk)
|
|
--tty N VT for the cage session, 1-12 (default: 2)
|
|
--librewolf FLAVOR Use LibreWolf: native | flatpak | auto
|
|
--firefox FLAVOR Use Firefox: native | flatpak | snap | auto
|
|
(default: auto-detect, LibreWolf preferred;
|
|
--librewolf and --firefox are mutually exclusive)
|
|
--usher-bin PATH Explicit usher binary to install
|
|
--start Start the kiosk session immediately
|
|
--yes Skip the confirmation prompt (unattended)
|
|
--quiet Minimal output
|
|
--check Validate prerequisites only; change nothing
|
|
-h, --help This text
|
|
|
|
Firefox notes: Firefox ESR is recommended for kiosk stability. The
|
|
extension, usher, and lockdown policies are identical for both browsers.
|
|
Vestibule configures an existing Firefox install; it never downloads or
|
|
redistributes Firefox (Mozilla trademark policy).
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
code="$1"; msg="$2"
|
|
echo ""
|
|
echo "ERROR: ${msg}" >&2
|
|
echo " exiting with code ${code}" >&2
|
|
exit "${code}"
|
|
}
|
|
info() { echo "$1"; }
|
|
ok() { echo " [ok] $1"; }
|
|
step() { echo ""; echo "==> $1"; }
|
|
|
|
# First executable in the argument list wins; empty arguments are skipped.
|
|
# Single home for every ordered-path probe in this script.
|
|
first_executable() {
|
|
for cand in "$@"; do
|
|
if [ -n "${cand}" ] && [ -x "${cand}" ]; then
|
|
printf '%s\n' "${cand}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ─── Argument parsing ─────────────────────────────────────────────────
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--home-url) HOME_URL="$2"; shift 2 ;;
|
|
--kiosk-user) KIOSK_USER="$2"; shift 2 ;;
|
|
--tty) TTY_NUM="$2"; shift 2 ;;
|
|
--librewolf) BROWSER_REQ="librewolf"; FLAVOR_REQ="$2"
|
|
BROWSER_FLAGS_SEEN="${BROWSER_FLAGS_SEEN} librewolf"; shift 2 ;;
|
|
--firefox) BROWSER_REQ="firefox"; FLAVOR_REQ="$2"
|
|
BROWSER_FLAGS_SEEN="${BROWSER_FLAGS_SEEN} firefox"; shift 2 ;;
|
|
--usher-bin) USHER_SRC="$2"; shift 2 ;;
|
|
--start) START_NOW=1; shift ;;
|
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
--quiet) QUIET=1; shift ;;
|
|
--check) CHECK=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) usage >&2; die 6 "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
if [ "$(printf '%s' "${BROWSER_FLAGS_SEEN}" | wc -w)" -gt 1 ]; then
|
|
die 6 "--librewolf and --firefox are mutually exclusive (saw:${BROWSER_FLAGS_SEEN})"
|
|
fi
|
|
|
|
if [ "${BROWSER_REQ}" = "librewolf" ]; then
|
|
case "${FLAVOR_REQ}" in
|
|
native|flatpak|auto) ;;
|
|
*) die 6 "--librewolf must be native, flatpak, or auto (got: ${FLAVOR_REQ})" ;;
|
|
esac
|
|
fi
|
|
if [ "${BROWSER_REQ}" = "firefox" ]; then
|
|
case "${FLAVOR_REQ}" in
|
|
native|flatpak|snap|auto) ;;
|
|
*) die 6 "--firefox must be native, flatpak, snap, or auto (got: ${FLAVOR_REQ})" ;;
|
|
esac
|
|
fi
|
|
|
|
case "${TTY_NUM}" in
|
|
''|*[!0-9]*) die 6 "--tty must be a number (got: ${TTY_NUM})" ;;
|
|
esac
|
|
[ "${TTY_NUM}" -ge 1 ] && [ "${TTY_NUM}" -le 12 ] || die 6 "--tty must be 1-12"
|
|
|
|
case "${HOME_URL}" in
|
|
http://*|https://*|about:*) ;;
|
|
*) die 6 "--home-url must start with http://, https://, or about: (got: ${HOME_URL})" ;;
|
|
esac
|
|
|
|
# ─── Detection ────────────────────────────────────────────────────────
|
|
|
|
step "Pre-flight detection"
|
|
|
|
if [ ! -d /run/systemd/system ]; then
|
|
die 2 "systemd is not the running init system — this provisioning path targets systemd"
|
|
fi
|
|
ok "systemd"
|
|
|
|
# Distro + package manager (informational: we print install commands,
|
|
# we never auto-install — operator stays in control of the base image).
|
|
# NOTE: os-release is sourced in a SUBSHELL — its standard fields
|
|
# (HOME_URL, SUPPORT_URL, ...) would otherwise clobber our variables.
|
|
DISTRO_ID="unknown"
|
|
PKG_MGR="none"
|
|
if [ -r /etc/os-release ]; then
|
|
DISTRO_ID=$(
|
|
# shellcheck disable=SC1091
|
|
. /etc/os-release
|
|
printf '%s' "${ID:-unknown}"
|
|
)
|
|
fi
|
|
for pm in apt-get dnf pacman zypper; do
|
|
if command -v "${pm}" >/dev/null 2>&1; then PKG_MGR="${pm}"; break; fi
|
|
done
|
|
[ "${QUIET}" -eq 1 ] || info "distro: ${DISTRO_ID} (pkg mgr: ${PKG_MGR})"
|
|
|
|
CAGE_BIN=""
|
|
if command -v cage >/dev/null 2>&1; then
|
|
CAGE_BIN=$(command -v cage)
|
|
elif [ -x /usr/bin/cage ]; then
|
|
CAGE_BIN="/usr/bin/cage"
|
|
fi
|
|
if [ -n "${CAGE_BIN}" ]; then
|
|
ok "cage: ${CAGE_BIN}"
|
|
fi
|
|
|
|
# ── LibreWolf detection ──────────────────────────────────────────────
|
|
|
|
# Native LibreWolf: binary + its distribution directory.
|
|
LW_NATIVE_BIN=$(first_executable \
|
|
"$(command -v librewolf 2>/dev/null || true)" \
|
|
/usr/lib/librewolf/librewolf \
|
|
/usr/local/lib/librewolf/librewolf \
|
|
/opt/librewolf/librewolf \
|
|
/usr/local/bin/librewolf || true)
|
|
LW_DIST=""
|
|
if [ -n "${LW_NATIVE_BIN}" ]; then
|
|
LW_REAL=$(readlink -f "${LW_NATIVE_BIN}" 2>/dev/null || echo "${LW_NATIVE_BIN}")
|
|
LW_REAL_DIR=$(dirname "${LW_REAL}")
|
|
# A real LibreWolf install dir carries application.ini + browser/;
|
|
# /usr/bin/librewolf may be a wrapper script or symlink — don't trust
|
|
# its directory unless those markers are there.
|
|
if [ -d "${LW_REAL_DIR}/distribution" ] || [ -f "${LW_REAL_DIR}/application.ini" ] \
|
|
|| [ -d "${LW_REAL_DIR}/browser" ]; then
|
|
LW_DIST="${LW_REAL_DIR}/distribution"
|
|
else
|
|
# Wrapper-script install: pick the first candidate whose parent dir
|
|
# exists — provisioning creates distribution/ inside it.
|
|
for dist in \
|
|
/usr/lib/librewolf/distribution \
|
|
/usr/share/librewolf/distribution \
|
|
/opt/librewolf/distribution; do
|
|
if [ -d "$(dirname "${dist}")" ]; then LW_DIST="${dist}"; break; fi
|
|
done
|
|
fi
|
|
ok "librewolf (native): ${LW_NATIVE_BIN} (policies: ${LW_DIST})"
|
|
fi
|
|
|
|
# Flatpak LibreWolf: system installation only — a per-user install would
|
|
# be invisible to the kiosk account.
|
|
LW_FLATPAK=0
|
|
if command -v flatpak >/dev/null 2>&1; then
|
|
if flatpak info --system "${LW_FLATPAK_APP}" >/dev/null 2>&1; then
|
|
LW_FLATPAK=1
|
|
ok "librewolf (flatpak, system): ${LW_FLATPAK_APP}"
|
|
elif flatpak info --user "${LW_FLATPAK_APP}" >/dev/null 2>&1; then
|
|
info " [!!] ${LW_FLATPAK_APP} is installed per-USER — the kiosk account cannot see it."
|
|
info " reinstall system-wide: flatpak install --system flathub ${LW_FLATPAK_APP}"
|
|
fi
|
|
fi
|
|
|
|
# ── Firefox detection ────────────────────────────────────────────────
|
|
|
|
# Native Firefox. Canonical distro paths first (Debian/Ubuntu:
|
|
# /usr/lib/firefox, Fedora: /usr/lib64/firefox, Arch: /usr/bin symlink),
|
|
# then tarball-style installs (/opt, /usr/local). command -v results
|
|
# that resolve into /snap or a flatpak export are routed to their own
|
|
# flavors below.
|
|
FF_NATIVE_BIN=""
|
|
FF_NATIVE_REAL=""
|
|
FF_DISTRO=0
|
|
FF_DIST=""
|
|
for cand in \
|
|
/usr/lib/firefox/firefox \
|
|
/usr/lib64/firefox/firefox \
|
|
/usr/bin/firefox \
|
|
/opt/firefox/firefox \
|
|
/usr/local/firefox/firefox \
|
|
/usr/local/bin/firefox \
|
|
"$(command -v firefox 2>/dev/null || true)"; do
|
|
if [ -z "${cand}" ] || [ ! -x "${cand}" ]; then
|
|
continue
|
|
fi
|
|
real=$(readlink -f "${cand}" 2>/dev/null || echo "${cand}")
|
|
case "${real}" in
|
|
/snap/*) continue ;; # snap firefox, handled below
|
|
*/flatpak/*|*/.local/share/flatpak/*) continue ;; # flatpak export
|
|
esac
|
|
case "${cand}" in
|
|
/usr/lib/firefox/*|/usr/lib64/firefox/*|/usr/bin/*|/usr/share/*)
|
|
FF_DISTRO=1 ;;
|
|
*) FF_DISTRO=0 ;;
|
|
esac
|
|
FF_NATIVE_BIN="${cand}"
|
|
FF_NATIVE_REAL="${real}"
|
|
break
|
|
done
|
|
|
|
if [ -n "${FF_NATIVE_BIN}" ]; then
|
|
if [ "${FF_DISTRO}" -eq 1 ]; then
|
|
# Distro package: /etc/firefox/policies is the canonical,
|
|
# update-safe location (Mozilla's documented Linux path).
|
|
FF_DIST="/etc/firefox/policies"
|
|
else
|
|
# Tarball-style install: policies live beside the binary, in the
|
|
# distribution/ directory — same mechanism as Windows.
|
|
FF_DIST="$(dirname "${FF_NATIVE_REAL}")/distribution"
|
|
fi
|
|
ok "firefox (native): ${FF_NATIVE_BIN} (policies: ${FF_DIST})"
|
|
fi
|
|
|
|
# Firefox snap (Ubuntu's default): the sandbox home is
|
|
# ~/snap/firefox/common — policies and NM manifests for the kiosk user
|
|
# go there. Detected via snap list or a /usr/bin/firefox wrapper that
|
|
# resolves into /snap.
|
|
FF_SNAP=0
|
|
if command -v snap >/dev/null 2>&1; then
|
|
if snap list firefox >/dev/null 2>&1; then
|
|
FF_SNAP=1
|
|
fi
|
|
fi
|
|
if [ "${FF_SNAP}" -eq 0 ]; then
|
|
ff_probe=""
|
|
if [ -e /usr/bin/firefox ]; then
|
|
ff_probe=$(readlink -f /usr/bin/firefox 2>/dev/null || true)
|
|
elif [ -e /snap/bin/firefox ]; then
|
|
ff_probe=$(readlink -f /snap/bin/firefox 2>/dev/null || true)
|
|
fi
|
|
case "${ff_probe}" in
|
|
/snap/*) FF_SNAP=1 ;;
|
|
esac
|
|
fi
|
|
if [ "${FF_SNAP}" -eq 1 ]; then
|
|
ok "firefox (snap): /snap/bin/firefox (policies: ~kiosk/snap/firefox/common/.mozilla/policies)"
|
|
fi
|
|
|
|
# Firefox Flatpak: system installation only.
|
|
FF_FLATPAK=0
|
|
if command -v flatpak >/dev/null 2>&1; then
|
|
if flatpak info --system "${FF_FLATPAK_APP}" >/dev/null 2>&1; then
|
|
FF_FLATPAK=1
|
|
ok "firefox (flatpak, system): ${FF_FLATPAK_APP}"
|
|
elif flatpak info --user "${FF_FLATPAK_APP}" >/dev/null 2>&1; then
|
|
info " [!!] ${FF_FLATPAK_APP} is installed per-USER — the kiosk account cannot see it."
|
|
info " reinstall system-wide: flatpak install --system flathub ${FF_FLATPAK_APP}"
|
|
fi
|
|
fi
|
|
|
|
# ── Resolve browser + flavor ─────────────────────────────────────────
|
|
#
|
|
# Step-down resolution: each probe names one flavor; the first available
|
|
# wins. An explicit --librewolf/--firefox flavor request pins the probe to
|
|
# that flavor alone; "auto" walks the whole ladder.
|
|
|
|
BROWSER=""
|
|
FLAVOR=""
|
|
|
|
flavor_available() {
|
|
# Lookup table: browser:flavor -> availability test.
|
|
case "$1:$2" in
|
|
librewolf:native) [ -n "${LW_NATIVE_BIN}" ] ;;
|
|
librewolf:flatpak) [ "${LW_FLATPAK}" -eq 1 ] ;;
|
|
firefox:native) [ -n "${FF_NATIVE_BIN}" ] ;;
|
|
firefox:flatpak) [ "${FF_FLATPAK}" -eq 1 ] ;;
|
|
firefox:snap) [ "${FF_SNAP}" -eq 1 ] ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
resolve_flavor() {
|
|
# $1 = browser, $2 = flavor
|
|
if [ "${FLAVOR_REQ}" = "auto" ] || [ "${FLAVOR_REQ}" = "$2" ]; then
|
|
if flavor_available "$1" "$2"; then
|
|
BROWSER="$1"; FLAVOR="$2"; return 0
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
resolve_librewolf() {
|
|
resolve_flavor librewolf native && return 0
|
|
resolve_flavor librewolf flatpak && return 0
|
|
return 1
|
|
}
|
|
|
|
resolve_firefox() {
|
|
resolve_flavor firefox native && return 0
|
|
resolve_flavor firefox flatpak && return 0
|
|
resolve_flavor firefox snap && return 0
|
|
return 1
|
|
}
|
|
|
|
if [ "${BROWSER_REQ}" = "librewolf" ]; then
|
|
resolve_librewolf
|
|
elif [ "${BROWSER_REQ}" = "firefox" ]; then
|
|
resolve_firefox
|
|
else
|
|
# Auto step-down: LibreWolf (privacy defaults + trademark-safe), then
|
|
# Firefox (fully supported alternative).
|
|
FLAVOR_REQ="auto"
|
|
resolve_librewolf || resolve_firefox
|
|
fi
|
|
|
|
BROWSER_MISSING=""
|
|
if [ -z "${BROWSER}" ]; then
|
|
if [ "${BROWSER_REQ}" = "librewolf" ]; then
|
|
BROWSER_MISSING="librewolf"
|
|
elif [ "${BROWSER_REQ}" = "firefox" ]; then
|
|
BROWSER_MISSING="firefox"
|
|
else
|
|
BROWSER_MISSING="browser (librewolf or firefox)"
|
|
fi
|
|
fi
|
|
|
|
# usher source: explicit flag > existing install > repo build > staged.
|
|
if [ -z "${USHER_SRC}" ]; then
|
|
if [ -x "${USHER_DST}" ]; then
|
|
USHER_SRC="${USHER_DST}"
|
|
elif [ -x "${PROJECT_ROOT}/helper/target/release/usher" ]; then
|
|
USHER_SRC="${PROJECT_ROOT}/helper/target/release/usher"
|
|
elif [ -x "${OPT_ROOT}/bin/usher" ]; then
|
|
USHER_SRC="${OPT_ROOT}/bin/usher"
|
|
fi
|
|
fi
|
|
|
|
PYTHON_BIN=""
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
PYTHON_BIN=$(command -v python3)
|
|
fi
|
|
|
|
DBUS_RUN=""
|
|
if command -v dbus-run-session >/dev/null 2>&1; then
|
|
DBUS_RUN=$(command -v dbus-run-session)
|
|
fi
|
|
|
|
# ─── Missing-prerequisite report ──────────────────────────────────────
|
|
|
|
MISSING=""
|
|
if [ -z "${CAGE_BIN}" ]; then MISSING="${MISSING} cage"; fi
|
|
if [ -n "${BROWSER_MISSING}" ]; then MISSING="${MISSING} ${BROWSER_MISSING}"; fi
|
|
if [ -z "${USHER_SRC}" ]; then MISSING="${MISSING} usher"; fi
|
|
if [ -z "${PYTHON_BIN}" ]; then MISSING="${MISSING} python3"; fi
|
|
if [ -z "${DBUS_RUN}" ]; then MISSING="${MISSING} dbus"; fi
|
|
|
|
if [ -n "${MISSING}" ]; then
|
|
echo ""
|
|
info "Missing prerequisites:${MISSING}"
|
|
info "Install them, then re-run this script. Per-distro commands:"
|
|
echo ""
|
|
case "${PKG_MGR}" in
|
|
apt-get)
|
|
echo " sudo apt-get install -y cage dbus python3 firefox-esr"
|
|
echo " # LibreWolf: deb repo — https://librewolf.net/installation/linux/"
|
|
echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}"
|
|
echo " # or Firefox Flatpak: flatpak install --system flathub ${FF_FLATPAK_APP}" ;;
|
|
dnf)
|
|
echo " sudo dnf install -y cage dbus python3 firefox"
|
|
echo " # LibreWolf: https://librewolf.net/installation/linux/"
|
|
echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;;
|
|
pacman)
|
|
echo " sudo pacman -S --needed cage dbus python3 firefox"
|
|
echo " # LibreWolf: AUR (librewolf / librewolf-bin)"
|
|
echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;;
|
|
zypper)
|
|
echo " sudo zypper install cage dbus python3 MozillaFirefox"
|
|
echo " # LibreWolf: https://librewolf.net/installation/linux/"
|
|
echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;;
|
|
*)
|
|
echo " # Install cage, dbus, python3, and a browser (firefox or"
|
|
echo " # librewolf) from your distribution."
|
|
echo " # cage from source: https://github.com/cage-kiosk/cage"
|
|
echo " # Flatpak browsers: flatpak install --system flathub ${FF_FLATPAK_APP}" ;;
|
|
esac
|
|
echo ""
|
|
echo " usher: build from source — cd helper && cargo build --release"
|
|
die 3 "prerequisites not met"
|
|
fi
|
|
ok "usher: ${USHER_SRC}"
|
|
ok "python3: ${PYTHON_BIN}"
|
|
ok "browser: ${BROWSER} (${FLAVOR})"
|
|
|
|
# ─── Policy destination preview (needs KIOSK_HOME for sandboxed flavors)
|
|
|
|
policy_dir_for_kiosk_home() {
|
|
# $1 = kiosk home
|
|
case "${BROWSER}:${FLAVOR}" in
|
|
librewolf:native) printf '%s' "${LW_DIST}" ;;
|
|
librewolf:flatpak) printf '%s' "/var/lib/flatpak/app/${LW_FLATPAK_APP}/current/active/files/librewolf/distribution" ;;
|
|
firefox:native) printf '%s' "${FF_DIST}" ;;
|
|
firefox:flatpak) printf '%s' "$1/.var/app/${FF_FLATPAK_APP}/.mozilla/policies" ;;
|
|
firefox:snap) printf '%s' "$1/snap/firefox/common/.mozilla/policies" ;;
|
|
esac
|
|
}
|
|
|
|
if [ "${CHECK}" -eq 1 ]; then
|
|
step "Check complete (no changes made)"
|
|
info "kiosk user : ${KIOSK_USER} (created if absent)"
|
|
info "tty : /dev/tty${TTY_NUM}"
|
|
info "browser : ${BROWSER} (${FLAVOR})"
|
|
info "home URL : ${HOME_URL}"
|
|
info "policies dir : $(policy_dir_for_kiosk_home "/home/${KIOSK_USER}")"
|
|
info "opt root : ${OPT_ROOT}"
|
|
ok "all prerequisites satisfied — re-run without --check to provision"
|
|
exit 0
|
|
fi
|
|
|
|
[ "$(id -u)" -eq 0 ] || die 2 "must run as root (sudo)"
|
|
|
|
# ─── Confirmation ─────────────────────────────────────────────────────
|
|
|
|
if [ "${ASSUME_YES}" -eq 0 ] && [ "${QUIET}" -eq 0 ]; then
|
|
echo ""
|
|
printf "Provision kiosk (user=%s, tty=%s, browser=%s/%s, url=%s)? [y/N] " \
|
|
"${KIOSK_USER}" "${TTY_NUM}" "${BROWSER}" "${FLAVOR}" "${HOME_URL}"
|
|
read -r answer
|
|
case "${answer}" in
|
|
y|Y|yes|YES) ;;
|
|
*) info "aborted"; exit 0 ;;
|
|
esac
|
|
fi
|
|
|
|
# ─── 1. Kiosk user ────────────────────────────────────────────────────
|
|
|
|
step "Kiosk user '${KIOSK_USER}'"
|
|
KIOSK_HOME=""
|
|
if id -u "${KIOSK_USER}" >/dev/null 2>&1; then
|
|
KIOSK_HOME=$(getent passwd "${KIOSK_USER}" | cut -d: -f6)
|
|
ok "exists (home: ${KIOSK_HOME})"
|
|
else
|
|
useradd -m -s /bin/sh "${KIOSK_USER}" || die 4 "useradd failed"
|
|
KIOSK_HOME=$(getent passwd "${KIOSK_USER}" | cut -d: -f6)
|
|
ok "created with locked password (no password login is possible)"
|
|
fi
|
|
[ -n "${KIOSK_HOME}" ] || die 4 "could not resolve home directory"
|
|
|
|
# ─── 2. Stage /opt/vestibule ──────────────────────────────────────────
|
|
|
|
step "Stage ${OPT_ROOT}"
|
|
mkdir -p "${OPT_ROOT}/bin" "${OPT_ROOT}/extension" "${OPT_ROOT}/config" "${OPT_ROOT}/docs"
|
|
|
|
install -m 0755 "${USHER_SRC}" "${OPT_ROOT}/bin/usher"
|
|
install -m 0755 "${USHER_SRC}" "${USHER_DST}"
|
|
ok "usher installed: ${USHER_DST}"
|
|
|
|
# Build the XPI (a zip of extension/) — python3 zipfile, no zip binary
|
|
# dependency. If an XPI is already staged and no source tree is present,
|
|
# keep the staged one.
|
|
XPI_SRC_DIR="${PROJECT_ROOT}/extension"
|
|
XPI_DST="${OPT_ROOT}/extension/vestibule.xpi"
|
|
if [ -f "${XPI_SRC_DIR}/manifest.json" ]; then
|
|
"${PYTHON_BIN}" - "${XPI_SRC_DIR}" "${XPI_DST}" <<'PYEOF'
|
|
import os, sys, zipfile
|
|
src_dir, dst = sys.argv[1], sys.argv[2]
|
|
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for root, dirs, files in os.walk(src_dir):
|
|
dirs.sort()
|
|
for f in sorted(files):
|
|
full = os.path.join(root, f)
|
|
z.write(full, os.path.relpath(full, src_dir))
|
|
PYEOF
|
|
chmod 0644 "${XPI_DST}"
|
|
ok "built extension XPI: ${XPI_DST}"
|
|
elif [ -f "${XPI_DST}" ]; then
|
|
ok "using staged XPI: ${XPI_DST}"
|
|
else
|
|
die 3 "no extension source at ${XPI_SRC_DIR} and no staged XPI"
|
|
fi
|
|
|
|
for f in config/policies.json config/librewolf.overrides.cfg \
|
|
config/vestibule.toml.example; do
|
|
if [ -f "${PROJECT_ROOT}/${f}" ]; then
|
|
install -m 0644 "${PROJECT_ROOT}/${f}" "${OPT_ROOT}/${f}"
|
|
fi
|
|
done
|
|
if [ -f "${PROJECT_ROOT}/LICENSE" ]; then install -m 0644 "${PROJECT_ROOT}/LICENSE" "${OPT_ROOT}/LICENSE"; fi
|
|
if [ -f "${PROJECT_ROOT}/DEPLOYMENT.md" ]; then install -m 0644 "${PROJECT_ROOT}/DEPLOYMENT.md" "${OPT_ROOT}/docs/DEPLOYMENT.md"; fi
|
|
if [ -f "${PROJECT_ROOT}/README.md" ]; then install -m 0644 "${PROJECT_ROOT}/README.md" "${OPT_ROOT}/docs/README.md"; fi
|
|
ok "staged config + docs"
|
|
|
|
# ─── 3. Launcher + kiosk.env ──────────────────────────────────────────
|
|
|
|
step "Launcher + session config"
|
|
install -m 0755 "${UNIT_SRC_DIR}/vestibule-kiosk-launch" "${LAUNCH_DST}"
|
|
ok "installed ${LAUNCH_DST}"
|
|
|
|
mkdir -p "${ENV_DIR}"
|
|
cat > "${ENV_DIR}/kiosk.env" <<EOF
|
|
# /etc/vestibule/kiosk.env — Vestibule kiosk session configuration.
|
|
# Edit this file to reconfigure; the unit re-reads it on every start.
|
|
VESTIBULE_HOME_URL="${HOME_URL}"
|
|
VESTIBULE_BROWSER="${BROWSER}"
|
|
VESTIBULE_BROWSER_FLAVOR="${FLAVOR}"
|
|
# cage flags: "-d" allows VT switching (admin escape hatch, cage >= 0.1.2).
|
|
# In a VM without GPU: add WLR_LIBINPUT_NO_DEVICES=1 and WLR_RENDERER=pixman
|
|
# as separate Environment entries in the systemd unit, not here.
|
|
VESTIBULE_CAGE_ARGS="-d"
|
|
EOF
|
|
chmod 0644 "${ENV_DIR}/kiosk.env"
|
|
ok "wrote ${ENV_DIR}/kiosk.env (browser: ${BROWSER}/${FLAVOR})"
|
|
|
|
# ─── 4. Native Messaging host for the kiosk user ──────────────────────
|
|
|
|
step "Native Messaging host (kiosk user)"
|
|
NM_MANIFEST_BODY=$(cat <<EOF
|
|
{
|
|
"name": "${HOST_NAME}",
|
|
"description": "Vestibule native helper",
|
|
"path": "${USHER_DST}",
|
|
"type": "stdio",
|
|
"allowed_extensions": ["${EXT_ID}"]
|
|
}
|
|
EOF
|
|
)
|
|
NM_DIR_REL="native-messaging-hosts"
|
|
# Every location a Gecko variant can read manifests from, relative to
|
|
# the kiosk home: LibreWolf native, Firefox native (~/.mozilla), the two
|
|
# Flatpaks (sandbox home remap), and the Firefox snap. Writing all five
|
|
# costs nothing and makes the flavor choice future-proof.
|
|
for base in \
|
|
".librewolf" \
|
|
".mozilla" \
|
|
".var/app/${LW_FLATPAK_APP}/.librewolf" \
|
|
".var/app/${FF_FLATPAK_APP}/.mozilla" \
|
|
"snap/firefox/common/.mozilla"; do
|
|
nm_dir="${KIOSK_HOME}/${base}/${NM_DIR_REL}"
|
|
mkdir -p "${nm_dir}"
|
|
printf '%s\n' "${NM_MANIFEST_BODY}" > "${nm_dir}/${HOST_NAME}.json"
|
|
chmod 0644 "${nm_dir}/${HOST_NAME}.json"
|
|
done
|
|
chown -R "${KIOSK_USER}:${KIOSK_USER}" "${KIOSK_HOME}/.librewolf" \
|
|
"${KIOSK_HOME}/.mozilla" "${KIOSK_HOME}/.var" "${KIOSK_HOME}/snap" 2>/dev/null || \
|
|
chown -R "${KIOSK_USER}" "${KIOSK_HOME}/.librewolf" "${KIOSK_HOME}/.mozilla"
|
|
ok "manifests installed for ${KIOSK_USER} (librewolf + firefox, native + flatpak + snap)"
|
|
|
|
# ─── 5. policies.json ─────────────────────────────────────────────────
|
|
|
|
step "${BROWSER} policies"
|
|
|
|
# For sandboxed flavors (flatpak/snap) the browser cannot read /opt —
|
|
# its filesystem is the kiosk home remap. Copy the XPI to a
|
|
# sandbox-visible path and point install_url there. Native flavors read
|
|
# /opt/vestibule directly.
|
|
XPI_INSTALL_URL="file://${XPI_DST}"
|
|
if [ "${FLAVOR}" = "flatpak" ]; then
|
|
if [ "${BROWSER}" = "firefox" ]; then
|
|
SANDBOX_APP_DIR="${KIOSK_HOME}/.var/app/${FF_FLATPAK_APP}"
|
|
else
|
|
SANDBOX_APP_DIR="${KIOSK_HOME}/.var/app/${LW_FLATPAK_APP}"
|
|
fi
|
|
mkdir -p "${SANDBOX_APP_DIR}"
|
|
install -m 0644 "${XPI_DST}" "${SANDBOX_APP_DIR}/vestibule.xpi"
|
|
chown -R "${KIOSK_USER}:${KIOSK_USER}" "${SANDBOX_APP_DIR}" 2>/dev/null || \
|
|
chown -R "${KIOSK_USER}" "${SANDBOX_APP_DIR}"
|
|
# Inside the sandbox $HOME is the kiosk home path — that is where the
|
|
# browser must find the XPI.
|
|
XPI_INSTALL_URL="file://${KIOSK_HOME}/vestibule.xpi"
|
|
ok "XPI copied to sandbox-visible ${SANDBOX_APP_DIR}/vestibule.xpi"
|
|
elif [ "${FLAVOR}" = "snap" ]; then
|
|
SANDBOX_APP_DIR="${KIOSK_HOME}/snap/firefox/common"
|
|
mkdir -p "${SANDBOX_APP_DIR}"
|
|
install -m 0644 "${XPI_DST}" "${SANDBOX_APP_DIR}/vestibule.xpi"
|
|
chown -R "${KIOSK_USER}:${KIOSK_USER}" "${SANDBOX_APP_DIR}" 2>/dev/null || \
|
|
chown -R "${KIOSK_USER}" "${SANDBOX_APP_DIR}"
|
|
XPI_INSTALL_URL="file://${KIOSK_HOME}/vestibule.xpi"
|
|
ok "XPI copied to snap-visible ${SANDBOX_APP_DIR}/vestibule.xpi"
|
|
fi
|
|
|
|
POLICY_DIR=$(policy_dir_for_kiosk_home "${KIOSK_HOME}")
|
|
mkdir -p "${POLICY_DIR}"
|
|
POLICY_DST="${POLICY_DIR}/policies.json"
|
|
if [ -f "${POLICY_DST}" ] && [ ! -f "${POLICY_DST}.vestibule-bak" ]; then
|
|
cp "${POLICY_DST}" "${POLICY_DST}.vestibule-bak"
|
|
ok "backed up existing policies.json -> vestibule-bak"
|
|
fi
|
|
|
|
"${PYTHON_BIN}" - "${PROJECT_ROOT}/config/policies.json" "${POLICY_DST}" \
|
|
"${XPI_INSTALL_URL}" "${EXT_ID}" <<'PYEOF'
|
|
import json, sys
|
|
canonical_path, target, xpi_url, ext_id = sys.argv[1:5]
|
|
|
|
policies = {"policies": {}}
|
|
try:
|
|
with open(target) as f:
|
|
existing = json.load(f)
|
|
if isinstance(existing, dict):
|
|
policies = existing
|
|
except (OSError, ValueError):
|
|
pass
|
|
|
|
with open(canonical_path) as f:
|
|
canonical = json.load(f)
|
|
|
|
def deep_merge(base, overlay):
|
|
for k, v in overlay.items():
|
|
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
|
|
deep_merge(base[k], v)
|
|
else:
|
|
base[k] = v
|
|
|
|
deep_merge(policies, canonical)
|
|
|
|
# Policy-install the extension: force_installed survives the "*" blocked
|
|
# wildcard and re-installs itself on every browser start.
|
|
policies.setdefault("policies", {})
|
|
policies["policies"]["ExtensionSettings"] = {
|
|
"*": {
|
|
"blocked_install_message": "Extensions are not allowed on this kiosk.",
|
|
"install_sources": [],
|
|
"installation_mode": "blocked",
|
|
},
|
|
ext_id: {
|
|
"installation_mode": "force_installed",
|
|
"install_url": xpi_url,
|
|
},
|
|
}
|
|
|
|
with open(target, "w") as f:
|
|
json.dump(policies, f, indent=2)
|
|
f.write("\n")
|
|
PYEOF
|
|
if [ -n "${POLICY_DIR##${KIOSK_HOME}*}" ]; then
|
|
# System-level policy file — root-owned is correct.
|
|
:
|
|
else
|
|
# Policy file inside the kiosk home (flatpak/snap flavors) — the
|
|
# browser reads it as the kiosk user.
|
|
chown "${KIOSK_USER}:${KIOSK_USER}" "${POLICY_DST}" 2>/dev/null || \
|
|
chown "${KIOSK_USER}" "${POLICY_DST}"
|
|
fi
|
|
ok "deployed ${POLICY_DST} (extension force-installed from ${XPI_INSTALL_URL})"
|
|
|
|
if [ "${FLAVOR}" = "flatpak" ] && [ "${BROWSER}" = "librewolf" ]; then
|
|
info "NOTE: the Flatpak app dir is replaced on every LibreWolf update."
|
|
info " re-run this script after updates (the extension's own session"
|
|
info " sanitization is unaffected — this file is layer 1 of 3)."
|
|
fi
|
|
if [ "${FLAVOR}" = "flatpak" ] || [ "${FLAVOR}" = "snap" ]; then
|
|
info "NOTE: for ${BROWSER} ${FLAVOR}, policies and the XPI live in the"
|
|
info " kiosk user's home — they survive browser updates (unlike a"
|
|
info " LibreWolf-Flatpak system-dir deploy)."
|
|
fi
|
|
|
|
# ─── 6. systemd unit ──────────────────────────────────────────────────
|
|
|
|
step "systemd unit"
|
|
sed -e "s|__KIOSK_USER__|${KIOSK_USER}|g" -e "s|__TTY__|${TTY_NUM}|g" \
|
|
"${UNIT_SRC_DIR}/vestibule-kiosk.service.in" > "${UNIT_DST}"
|
|
chmod 0644 "${UNIT_DST}"
|
|
ok "generated ${UNIT_DST} (tty${TTY_NUM}, user ${KIOSK_USER})"
|
|
|
|
systemctl daemon-reload || die 5 "systemctl daemon-reload failed"
|
|
systemctl enable vestibule-kiosk.service >/dev/null 2>&1 || die 5 "enable failed"
|
|
ok "enabled vestibule-kiosk.service"
|
|
|
|
if [ "${START_NOW}" -eq 1 ]; then
|
|
systemctl start vestibule-kiosk.service || die 5 "start failed — check: journalctl -u vestibule-kiosk.service"
|
|
ok "started — the kiosk should be running on tty${TTY_NUM}"
|
|
else
|
|
info "start now with: sudo systemctl start vestibule-kiosk.service"
|
|
info "or reboot — the unit starts automatically at boot."
|
|
fi
|
|
|
|
# ─── Summary ──────────────────────────────────────────────────────────
|
|
|
|
step "Provisioning complete"
|
|
info "kiosk user : ${KIOSK_USER} (locked password)"
|
|
info "session : cage on tty${TTY_NUM} via systemd"
|
|
info "browser : ${BROWSER} (${FLAVOR})"
|
|
info "home URL : ${HOME_URL}"
|
|
info "policies : ${POLICY_DST}"
|
|
info "logs : journalctl -u vestibule-kiosk.service -f"
|
|
info "escape hatch : Ctrl+Alt+F3 (VT switching; requires VESTIBULE_CAGE_ARGS=-d)"
|
|
exit 0
|