Third and biggest Addon migrated: menus/addon_remote_access.sh - VNC (x11vnc), WireGuard, Tailscale, and Netbird, each with its own install/ connect/status/uninstall flow. Same risk class as CUPS (real apt packages, real system state) but broader in scope: Tailscale and Netbird install via the vendors' own documented `curl -fsSL <url> | sh` method, preserved exactly as-is rather than redesigned. - lib/config.sh: new $WIREGUARD_DIR, same pattern as $SYSTEMD_DIR/ $BIN_DIR/etc - nothing in this file hardcodes /etc/wireguard. - lib/menu.sh: promoted power_schedule.sh's enable_and_start_timers() to a shared enable_and_start_units() (works for services now too, not just timers) - Remote Access needed the identical enable+start-with- graceful-failure-reporting pattern for x11vnc and wg-quick@, so this is fixed once and reused rather than duplicated a second time. power_schedule.sh's four call sites renamed to match. Found and fixed a real framework-level bug while building this file: run_menu()'s *handler* call has been `|| true`-guarded since v2.1.0, but the *status function* call (`"$status_func"` on its own line) was still completely bare. A status function's entire job is read-only display, but if it contains so much as a pipeline whose grep matches nothing - which pipefail turns into a pipeline failure even though the actual last command in it (e.g. sed) succeeds - that bare call would crash the *entire session*, not just fail to show status text. Found while writing wireguard_status()'s `sudo wg show | grep ... | sed ...` and deliberately verifying its exact failure mode rather than assuming run_menu already covered it. Fixed once in run_menu() itself (lib/menu.sh), protecting every status function across every menu - present and future - the same "fix once at the framework level" pattern as the v2.1.0 handler fix. Given the framework fix meant this class of bug had been silently possible since v2.1.0, audited every existing status function across every already-migrated menu for the same specific shape (a bare `var=$(...)` assignment from a grep-based pipeline, not embedded in an echo and not already guarded - embedded substitutions and if-condition contexts are both already safe on their own). Found and fixed one real instance in power_schedule_status(). menus/addon_remote_access.sh's own two equivalent pipelines (wireguard_status, netbird_status) were written with `|| true` from the start once the pattern was identified. Verified: - New scratch/stub test for addon_remote_access.sh, with curl stubbed separately from sudo (Tailscale/Netbird's install scripts must never reach the real network regardless of what sudo intercepts) and a belt-and-suspenders `sh` stub in case anything got past curl: full status/menu-builder coverage for all four sub-areas in their real, unstubbed "not installed" state (none of the four tools exist in this sandbox); VNC install/change-password/uninstall with systemd unit content verified (correct $KIOSK_USER/$KIOSK_HOME substitution); WireGuard install, paste-config (content written correctly to scratch $WIREGUARD_DIR), and uninstall - including documenting a genuine cat-until-EOF test-harness limitation (a redirected pipe's EOF is permanent for the whole stream, unlike a real terminal's per-read Ctrl+D, so only the config's *default* name is testable through simple stdin redirection - inherent to the design, matches the legacy script's identical `cat`-based approach, not a bug); Tailscale and Netbird install/connect-interactive/connect-with-key/uninstall; and all four cancel paths confirmed to make zero sudo calls. - Full regression: re-ran all 11 prior scratch/stub suites after the lib/menu.sh and power_schedule.sh changes - all still clean. - End-to-end: ran the real install.sh as a genuine non-root, non- "kiosk" user through Addons -> Remote Access -> all four sub-menus in turn, each showing accurate real (unstubbed) "not installed" status, selecting Install, declining the confirmation, and returning cleanly - zero invalid-choice errors, clean exit code 0.
251 lines
12 KiB
Bash
251 lines
12 KiB
Bash
#!/bin/bash
|
|
################################################################################
|
|
# lib/config.sh - config.json read/write for the kiosk Electron app.
|
|
#
|
|
# Every menu that touches sites/timing/settings works against the same
|
|
# in-memory bash arrays (URLS, DURS, USERS, PASSES, NAMES, ...) and the same
|
|
# two functions below. Load once when a menu opens, save after each change.
|
|
#
|
|
# IMPORTANT: save_config() rewrites config.json from these globals in full.
|
|
# Any menu that calls save_config() MUST call load_existing_config() first
|
|
# (even if it only touches sites), otherwise settings it doesn't know about
|
|
# (swipe mode, navigation security, lockout, etc) get silently reset to
|
|
# script defaults. This bit the old single-file Sites menu, which read
|
|
# tabs directly via jq but never loaded the rest of the settings - fixed
|
|
# here by making load_existing_config() the one canonical loader.
|
|
################################################################################
|
|
|
|
: "${KIOSK_USER:=kiosk}"
|
|
: "${KIOSK_HOME:=/home/${KIOSK_USER}}"
|
|
: "${KIOSK_DIR:=${KIOSK_HOME}/kiosk-app}"
|
|
: "${CONFIG_PATH:=${KIOSK_DIR}/config.json}"
|
|
|
|
# System paths that menus (e.g. power/display/quiet-hours scheduling)
|
|
# write units, scripts, and cron entries into. Overridable so tests can
|
|
# point them at a scratch directory instead of the real system - nothing
|
|
# under menus/ should ever hardcode /etc/systemd/system, /etc/cron.d, or
|
|
# /usr/local/bin directly.
|
|
: "${SYSTEMD_DIR:=/etc/systemd/system}"
|
|
: "${CRON_D_DIR:=/etc/cron.d}"
|
|
: "${BIN_DIR:=/usr/local/bin}"
|
|
: "${NETPLAN_DIR:=/etc/netplan}"
|
|
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
|
|
: "${WIREGUARD_DIR:=/etc/wireguard}"
|
|
|
|
# The admin account actually running this tool (as opposed to $KIOSK_USER,
|
|
# the kiosk's own restricted account) - used where an addon needs to grant
|
|
# *this* user a group membership (e.g. lpadmin for CUPS).
|
|
: "${BUILD_USER:=${SUDO_USER:-$(whoami)}}"
|
|
|
|
# Site/tab arrays
|
|
declare -a URLS=()
|
|
declare -a DURS=()
|
|
declare -a USERS=()
|
|
declare -a PASSES=()
|
|
declare -a NAMES=()
|
|
|
|
# Other top-level config.json settings we must round-trip even though the
|
|
# Sites menu doesn't edit most of them.
|
|
AUTOSWITCH="true"
|
|
SWIPE_MODE="dual"
|
|
ALLOW_NAVIGATION="same-origin"
|
|
HOME_TAB_INDEX=-1
|
|
INACTIVITY_TIMEOUT=120
|
|
ENABLE_PAUSE_BUTTON="true"
|
|
ENABLE_KEYBOARD_BUTTON="true"
|
|
ENABLE_NAV_BUTTON="true"
|
|
ENABLE_PASSWORD_PROTECTION="false"
|
|
LOCKOUT_PASSWORD=""
|
|
LOCKOUT_TIMEOUT=0
|
|
LOCKOUT_AT_TIME=""
|
|
LOCKOUT_ACTIVE_START=""
|
|
LOCKOUT_ACTIVE_END=""
|
|
REQUIRE_PASSWORD_ON_BOOT="false"
|
|
AUTHELIA_URL=""
|
|
AUTHELIA_USERNAME=""
|
|
AUTHELIA_ENCRYPTED_PASSWORD=""
|
|
|
|
kiosk_user_exists() {
|
|
id "$KIOSK_USER" &>/dev/null
|
|
}
|
|
|
|
is_kiosk_installed() {
|
|
kiosk_user_exists && sudo -u "$KIOSK_USER" test -f "$KIOSK_DIR/main.js" 2>/dev/null
|
|
}
|
|
|
|
is_service_active() {
|
|
local service="$1"
|
|
systemctl is-active --quiet "$service" 2>/dev/null
|
|
}
|
|
|
|
# Load every setting config.json has into the bash globals above.
|
|
# Safe to call with no existing config file - leaves script defaults in place.
|
|
load_existing_config() {
|
|
if ! sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
URLS=()
|
|
DURS=()
|
|
USERS=()
|
|
PASSES=()
|
|
NAMES=()
|
|
|
|
local tab_count
|
|
tab_count=$(sudo -u "$KIOSK_USER" jq -r '.tabs | length' "$CONFIG_PATH" 2>/dev/null || echo "0")
|
|
if [[ "$tab_count" -gt 0 ]]; then
|
|
for ((i = 0; i < tab_count; i++)); do
|
|
URLS+=("$(sudo -u "$KIOSK_USER" jq -r ".tabs[$i].url" "$CONFIG_PATH")")
|
|
DURS+=("$(sudo -u "$KIOSK_USER" jq -r ".tabs[$i].duration" "$CONFIG_PATH")")
|
|
USERS+=("$(sudo -u "$KIOSK_USER" jq -r ".tabs[$i].username // empty" "$CONFIG_PATH")")
|
|
PASSES+=("$(sudo -u "$KIOSK_USER" jq -r ".tabs[$i].password // empty" "$CONFIG_PATH")")
|
|
NAMES+=("$(sudo -u "$KIOSK_USER" jq -r ".tabs[$i].name // empty" "$CONFIG_PATH")")
|
|
done
|
|
fi
|
|
|
|
HOME_TAB_INDEX=$(sudo -u "$KIOSK_USER" jq -r '.homeTabIndex // -1' "$CONFIG_PATH" 2>/dev/null || echo "-1")
|
|
INACTIVITY_TIMEOUT=$(sudo -u "$KIOSK_USER" jq -r '.inactivityTimeout // 120' "$CONFIG_PATH" 2>/dev/null || echo "120")
|
|
ALLOW_NAVIGATION=$(sudo -u "$KIOSK_USER" jq -r '.allowNavigation // "same-origin"' "$CONFIG_PATH" 2>/dev/null || echo "same-origin")
|
|
SWIPE_MODE=$(sudo -u "$KIOSK_USER" jq -r '.swipeMode // "dual"' "$CONFIG_PATH" 2>/dev/null || echo "dual")
|
|
|
|
local pause_btn keyboard_btn nav_btn password_enabled boot_password
|
|
pause_btn=$(sudo -u "$KIOSK_USER" jq -r '.enablePauseButton // true' "$CONFIG_PATH" 2>/dev/null)
|
|
[[ "$pause_btn" == "true" ]] && ENABLE_PAUSE_BUTTON="true" || ENABLE_PAUSE_BUTTON="false"
|
|
|
|
keyboard_btn=$(sudo -u "$KIOSK_USER" jq -r '.enableKeyboardButton // true' "$CONFIG_PATH" 2>/dev/null)
|
|
[[ "$keyboard_btn" == "true" ]] && ENABLE_KEYBOARD_BUTTON="true" || ENABLE_KEYBOARD_BUTTON="false"
|
|
|
|
nav_btn=$(sudo -u "$KIOSK_USER" jq -r '.enableNavButton // true' "$CONFIG_PATH" 2>/dev/null)
|
|
[[ "$nav_btn" == "true" ]] && ENABLE_NAV_BUTTON="true" || ENABLE_NAV_BUTTON="false"
|
|
|
|
password_enabled=$(sudo -u "$KIOSK_USER" jq -r '.enablePasswordProtection // false' "$CONFIG_PATH" 2>/dev/null)
|
|
[[ "$password_enabled" == "true" ]] && ENABLE_PASSWORD_PROTECTION="true" || ENABLE_PASSWORD_PROTECTION="false"
|
|
|
|
LOCKOUT_PASSWORD=$(sudo -u "$KIOSK_USER" jq -r '.lockoutPassword // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
LOCKOUT_TIMEOUT=$(sudo -u "$KIOSK_USER" jq -r '.lockoutTimeout // 0' "$CONFIG_PATH" 2>/dev/null || echo "0")
|
|
LOCKOUT_AT_TIME=$(sudo -u "$KIOSK_USER" jq -r '.lockoutAtTime // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
LOCKOUT_ACTIVE_START=$(sudo -u "$KIOSK_USER" jq -r '.lockoutActiveStart // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
LOCKOUT_ACTIVE_END=$(sudo -u "$KIOSK_USER" jq -r '.lockoutActiveEnd // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
|
|
boot_password=$(sudo -u "$KIOSK_USER" jq -r '.requirePasswordOnBoot // false' "$CONFIG_PATH" 2>/dev/null)
|
|
[[ "$boot_password" == "true" ]] && REQUIRE_PASSWORD_ON_BOOT="true" || REQUIRE_PASSWORD_ON_BOOT="false"
|
|
|
|
AUTHELIA_URL=$(sudo -u "$KIOSK_USER" jq -r '.autheliaURL // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
AUTHELIA_USERNAME=$(sudo -u "$KIOSK_USER" jq -r '.autheliaUsername // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
AUTHELIA_ENCRYPTED_PASSWORD=$(sudo -u "$KIOSK_USER" jq -r '.autheliaEncryptedPassword // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
|
|
}
|
|
|
|
# Write every bash global back out to config.json, then offer to reload the
|
|
# kiosk display so the change takes effect immediately.
|
|
save_config() {
|
|
if ! kiosk_user_exists; then
|
|
log_error "Kiosk user doesn't exist - run the full installer first"
|
|
return 1
|
|
fi
|
|
|
|
sudo mkdir -p "$KIOSK_DIR"
|
|
sudo chown -R "$KIOSK_USER:$KIOSK_USER" "$KIOSK_DIR"
|
|
|
|
local tmp
|
|
tmp=$(mktemp)
|
|
|
|
local dual_json="false"
|
|
[[ "$SWIPE_MODE" == "dual" ]] && dual_json="true"
|
|
|
|
local pause_btn_json="true"
|
|
[[ "$ENABLE_PAUSE_BUTTON" == "false" ]] && pause_btn_json="false"
|
|
|
|
local keyboard_btn_json="true"
|
|
[[ "$ENABLE_KEYBOARD_BUTTON" == "false" ]] && keyboard_btn_json="false"
|
|
|
|
local nav_btn_json="true"
|
|
[[ "$ENABLE_NAV_BUTTON" == "false" ]] && nav_btn_json="false"
|
|
|
|
local password_json="false"
|
|
[[ "$ENABLE_PASSWORD_PROTECTION" == "true" ]] && password_json="true"
|
|
|
|
local boot_password_json="false"
|
|
[[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]] && boot_password_json="true"
|
|
|
|
# Merge onto whatever's already in config.json rather than rebuilding
|
|
# the file from nothing. The legacy save_config did a full `jq -n`
|
|
# rebuild listing every known field - any field it doesn't know about
|
|
# (e.g. Authelia's autheliaURL/autheliaUsername/
|
|
# autheliaEncryptedPassword, written by its own careful `. + {...}`
|
|
# merge) gets silently DELETED the next time any other menu that
|
|
# calls save_config runs. Real, currently-shipping bug in the legacy
|
|
# script, not unique to this migration - ported faithfully into this
|
|
# file's first version because no test happened to set an untracked
|
|
# field first. `. + {known fields...}` below preserves anything this
|
|
# tool doesn't track while still fully replacing every field it does
|
|
# (including tabs, via the same array-rebuild loop as before) - jq's
|
|
# `+` on objects takes the right-hand value for any key present on
|
|
# both sides, so a fully-specified `tabs` here still discards a
|
|
# deleted tab rather than merging old and new.
|
|
local existing="{}"
|
|
if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
|
|
existing=$(sudo -u "$KIOSK_USER" cat "$CONFIG_PATH" 2>/dev/null)
|
|
echo "$existing" | jq empty 2>/dev/null || existing="{}"
|
|
fi
|
|
|
|
echo "$existing" | jq \
|
|
--argjson autoswitch true \
|
|
--argjson enableTouch true \
|
|
--argjson dualSwipe "$dual_json" \
|
|
--arg swipeMode "$SWIPE_MODE" \
|
|
--arg allowNavigation "$ALLOW_NAVIGATION" \
|
|
--argjson homeTabIndex "${HOME_TAB_INDEX:--1}" \
|
|
--argjson inactivityTimeout "${INACTIVITY_TIMEOUT:-120}" \
|
|
--argjson enablePauseButton "$pause_btn_json" \
|
|
--argjson enableKeyboardButton "$keyboard_btn_json" \
|
|
--argjson enableNavButton "$nav_btn_json" \
|
|
--argjson enablePasswordProtection "$password_json" \
|
|
--arg lockoutPassword "${LOCKOUT_PASSWORD:-}" \
|
|
--argjson lockoutTimeout "${LOCKOUT_TIMEOUT:-0}" \
|
|
--arg lockoutAtTime "${LOCKOUT_AT_TIME:-}" \
|
|
--arg lockoutActiveStart "${LOCKOUT_ACTIVE_START:-}" \
|
|
--arg lockoutActiveEnd "${LOCKOUT_ACTIVE_END:-}" \
|
|
--argjson requirePasswordOnBoot "$boot_password_json" \
|
|
--arg autheliaURL "${AUTHELIA_URL:-}" \
|
|
--arg autheliaUsername "${AUTHELIA_USERNAME:-}" \
|
|
--arg autheliaEncryptedPassword "${AUTHELIA_ENCRYPTED_PASSWORD:-}" \
|
|
'. + {autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,autheliaURL:$autheliaURL,autheliaUsername:$autheliaUsername,autheliaEncryptedPassword:$autheliaEncryptedPassword,tabs:[]}' > "$tmp"
|
|
|
|
if [[ ${#URLS[@]} -gt 0 ]]; then
|
|
for idx in "${!URLS[@]}"; do
|
|
local url="${URLS[$idx]:-}"
|
|
local dur="${DURS[$idx]:-0}"
|
|
local user="${USERS[$idx]:-}"
|
|
local pass="${PASSES[$idx]:-}"
|
|
local name="${NAMES[$idx]:-}"
|
|
|
|
jq --arg u "$url" \
|
|
--argjson d "$dur" \
|
|
--arg user "$user" \
|
|
--arg pass "$pass" \
|
|
--arg name "$name" \
|
|
'.tabs += [{"url":$u,"duration":$d,"username":$user,"password":$pass,"name":$name}]' \
|
|
"$tmp" > "${tmp}.new"
|
|
mv -f "${tmp}.new" "$tmp"
|
|
done
|
|
fi
|
|
|
|
sudo -u "$KIOSK_USER" bash -c "cat > '$CONFIG_PATH'" < "$tmp"
|
|
sudo -u "$KIOSK_USER" chmod 644 "$CONFIG_PATH"
|
|
rm -f "$tmp"
|
|
|
|
log_success "Configuration saved"
|
|
|
|
if is_service_active lightdm; then
|
|
echo
|
|
if ask_yes_no "Reload kiosk now to apply changes?" "y"; then
|
|
echo "Reloading kiosk..."
|
|
sudo systemctl restart lightdm
|
|
sleep 2
|
|
log_success "Kiosk reloaded"
|
|
else
|
|
log_warning "Remember to reload: sudo systemctl restart lightdm"
|
|
fi
|
|
fi
|
|
}
|