diff --git a/README.md b/README.md index d3e1317..97c7a40 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,6 @@ sudo ./install.sh sudo reboot ``` -The installer performs preflight checks, backs up managed configuration under -`/var/backups/pivilion`, validates the deployed services, and restores the -previous configuration if deployment fails. Additional maintenance modes are: - -```sh -./install.sh --dry-run # read-only preflight and proposed actions -sudo ./install.sh --check # validate an existing installation -sudo ./install.sh --repair # repeat deployment without replacing gallery content -``` - The installer defaults to the user that invoked `sudo`, not to a hard-coded `pi` account. To customize paths, startup mode, hotspot SSID, or Wi-Fi interface, copy `pivilion.conf.example` to `/boot/firmware/pivilion.conf` (or @@ -52,22 +42,3 @@ Use `pikey` to save upstream Wi-Fi credentials, then `onion` and reboot to host through Tor. Use `hotspot` and reboot to run the local captive gallery at `http://10.1.1.1/`. HTTPS interception is intentionally not attempted because a local captive portal cannot present valid certificates for arbitrary sites. - -If a phone joins but does not open its captive-login window, run -`pivilion diagnose` on the Pi to verify DHCP/DNS, Apache rewrite handling, -nftables, and the common Android, Apple, and Microsoft HTTP probes. - -`pivilion status` distinguishes the requested next-boot mode from the last -verified active mode and reports degraded activation. `pivilion diagnose ---json` provides the same health checks for monitoring. The legacy -`pivilion-diagnose` command remains an alias. - -On the physical TTY, Pivilion appends its current IP, ASCII logo, and system -status through `/etc/issue.d/90-pivilion.issue`; Raspberry Pi OS retains its -normal Debian/hostname/TTY heading. The banner appears before `login:` rather -than among systemd boot messages. SSH sessions receive the same status once -after authentication. - -Diagnostic exit codes are stable: `10` invalid configuration, `11` missing or -unsupported hardware, `12` service/port conflict, `13` core service failure, -`14` captive portal failure, `15` Tor/onion failure, and `20` installer failure. diff --git a/etc/profile.d/pivilion-motd.sh b/etc/profile.d/pivilion-motd.sh deleted file mode 100644 index 655be9d..0000000 --- a/etc/profile.d/pivilion-motd.sh +++ /dev/null @@ -1,13 +0,0 @@ -# TTY consoles receive the banner from agetty before login. Keep the -# post-authentication banner only for interactive SSH sessions. -[ -n "${BASH_VERSION:-}" ] || return 0 -case $- in *i*) ;; *) return 0 ;; esac -[ -n "${SSH_CONNECTION:-}" ] || return 0 -[ -r /usr/local/lib/pivilion/common.sh ] || return 0 -[ "${PIVILION_MOTD_SHOWN:-0}" != 1 ] || return 0 - -. /usr/local/lib/pivilion/common.sh -pivilion_load_config >/dev/null 2>&1 || return 0 -[ "$(id -un)" = "$PIVILION_USER" ] || return 0 -pivilion_print_motd -export PIVILION_MOTD_SHOWN=1 diff --git a/etc/systemd/system/pivilion-hostapd.service b/etc/systemd/system/pivilion-hostapd.service deleted file mode 100644 index 02ba450..0000000 --- a/etc/systemd/system/pivilion-hostapd.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Pivilion legacy Wi-Fi access point -After=network-pre.target - -[Service] -Type=simple -ExecStart=/usr/sbin/hostapd /run/pivilion/hostapd.conf -Restart=on-failure - -[Install] -WantedBy=multi-user.target diff --git a/etc/systemd/system/pivilion-login-banner.service b/etc/systemd/system/pivilion-login-banner.service deleted file mode 100644 index aa386df..0000000 --- a/etc/systemd/system/pivilion-login-banner.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Generate the Pivilion TTY login banner -After=local-fs.target -Before=getty@tty1.service - -[Service] -Type=oneshot -ExecStart=/usr/local/lib/pivilion/issue-banner.sh -TimeoutStartSec=5 -StandardOutput=null -StandardError=journal - -[Install] -WantedBy=multi-user.target diff --git a/etc/systemd/system/pivilion-mode.service b/etc/systemd/system/pivilion-mode.service index 7f01a33..4b41d2d 100644 --- a/etc/systemd/system/pivilion-mode.service +++ b/etc/systemd/system/pivilion-mode.service @@ -7,7 +7,7 @@ Wants=network-pre.target Type=oneshot ExecStart=/usr/local/lib/pivilion/mode.sh RemainAfterExit=yes -TimeoutStartSec=120 +TimeoutStartSec=90 [Install] WantedBy=multi-user.target diff --git a/home/pi/.bashrc b/home/pi/.bashrc index 8f4a3be..6700e9e 100644 --- a/home/pi/.bashrc +++ b/home/pi/.bashrc @@ -112,13 +112,27 @@ if ! shopt -oq posix; then fi fi -# Pivilion login banner. The modern installer deploys the same hook through -# /etc/profile.d; this fallback keeps the legacy filesystem overlay usable. -if [ "${PIVILION_MOTD_SHOWN:-0}" != 1 ] && [ -r /usr/local/lib/pivilion/common.sh ]; then - . /usr/local/lib/pivilion/common.sh - if pivilion_load_config >/dev/null 2>&1 && [ "$(id -un)" = "$PIVILION_USER" ]; then - pivilion_print_motd - export PIVILION_MOTD_SHOWN=1 - fi -fi +# info on login + +let upSeconds="$(/usr/bin/cut -d. -f1 /proc/uptime)" +let secs=$((${upSeconds}%60)) +let mins=$((${upSeconds}/60%60)) +let hours=$((${upSeconds}/3600%24)) +let days=$((${upSeconds}/86400)) +UPTIME=`printf "%d days, %02dh%02dm%02ds" "$days" "$hours" "$mins" "$secs"` + +# get the load averages +read one five fifteen rest < /proc/loadavg +echo " +@@@@@@@@@@@@@@@@@@@ `date +"%A, %e %B %Y, %r"` +@@@@@@, &@& *@@@@@@ `uname -srnmo` +@@@@@@@. .@@@@@@@ Uptime.............: ${UPTIME} +@@@@ .@@@. @@@@ Memory.............: `cat /proc/meminfo | grep MemFree | awk {'print $2'}`kB (Free) / `cat /proc/meminfo | grep MemTotal | awk {'print $2'}`kB (Total) +@ &@@@@.@@@@# @ Load Averages......: ${one}, ${five}, ${fifteen} (1, 5, 15 min) +@ @ . #@@@# . @ @ Running Processes..: `ps ax | wc -l | tr -d " "` +@ @@@, # ,@@@ @ Manual / more info.: pivilion.net +@@, @ ,@@ Onion Address......: `cat /home/pi/pivilion/torname` +@@@@@#@@@@@@@#@@@@@ Run "pivilion" for inital setup! / Running in `cat /home/pi/pivilion/mode` mode +" export LC_ALL=C + diff --git a/home/pi/pivilion/config/html/.htaccess b/home/pi/pivilion/config/html/.htaccess index 1a1eb29..0e39710 100644 --- a/home/pi/pivilion/config/html/.htaccess +++ b/home/pi/pivilion/config/html/.htaccess @@ -1,11 +1,17 @@ RewriteEngine on - -# Captive-network probes use otherwise unknown paths such as /generate_204, -# /hotspot-detect.html, and /connecttest.txt. Redirect those temporarily so -# clients open their login assistant, but let the gallery serve its own PHP and -# asset paths without a loop. -RewriteCond %{REQUEST_URI} !\.php$ [NC] -RewriteCond %{REQUEST_URI} !\.head(?:\.|$) [NC] -RewriteCond %{REQUEST_URI} !^/(?:content|css|doc|docker|img|images|js|modules|tests|upload)(?:/|$) [NC] -RewriteCond %{REQUEST_URI} !^/pi-logo_128\.png$ [NC] -RewriteRule ^ http://10.1.1.1/index.php [L,R=302,NE] +RewriteCond %{REQUEST_URI} !(\/index\.php)$ +RewriteCond %{REQUEST_URI} !(\/pi-logo_128\.png)$ +RewriteCond %{REQUEST_URI} !(\/content\/.*)$ +RewriteCond %{REQUEST_URI} !(\/index\.php)$ +RewriteCond %{REQUEST_URI} !(\/pi-logo_128\.png)$ +RewriteCond %{REQUEST_URI} !(\/content\/.*)$ +RewriteCond %{REQUEST_URI} !(\/css\/.*)$ +RewriteCond %{REQUEST_URI} !(\/doc\/.*)$ +RewriteCond %{REQUEST_URI} !(\/docker\/.*)$ +RewriteCond %{REQUEST_URI} !(\/img\/.*)$ +RewriteCond %{REQUEST_URI} !(\/js\/.*)$ +RewriteCond %{REQUEST_URI} !(\/modules\/.*)$ +RewriteCond %{REQUEST_URI} !(\/tests\/.*)$ +RewriteCond %{REQUEST_URI} !(\/*.php)$ +RewriteCond %{REQUEST_URI} !(\/*.head.*)$ +RewriteRule ^(.*)$ http://10.1.1.1/index.php [L,R=301] diff --git a/install.sh b/install.sh index 69ea6d6..03f7984 100755 --- a/install.sh +++ b/install.sh @@ -1,184 +1,51 @@ #!/bin/bash -set -Eeuo pipefail +set -euo pipefail -INSTALL_VERSION=2 -ACTION=install -case "${1:-}" in - '') ;; - --check) ACTION=check ;; - --dry-run) ACTION=dry-run ;; - --repair) ACTION=repair ;; - -h|--help) echo "Usage: sudo ./install.sh [--check|--dry-run|--repair]"; exit 0 ;; - *) echo "Usage: sudo ./install.sh [--check|--dry-run|--repair]" >&2; exit 2 ;; -esac -[ "$#" -le 1 ] || { echo "Only one installer action may be specified." >&2; exit 2; } +if [ "$(id -u)" -ne 0 ]; then + echo "Run this installer with sudo: sudo ./install.sh" >&2 + exit 1 +fi SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" -pivilion_load_config || exit "$PIVILION_E_CONFIG" - -cat <<'PIVILION_LOGO' - -Welcome to Pivilion! - -@@@@@@@@@@@@@@@@@@@ -@@@@@@, &@& *@@@@@@ -@@@@@@@. .@@@@@@@ -@@@@ .@@@. @@@@ -@ &@@@@.@@@@# @ -@ @ . #@@@# . @ @ -@ @@@, # ,@@@ @ -@@, @ ,@@ -@@@@@#@@@@@@@#@@@@@ - -PIVILION_LOGO - -if [ "$ACTION" != dry-run ] && [ "$(id -u)" -ne 0 ]; then - echo "Run this installer with sudo: sudo ./install.sh ${1:-}" >&2 - exit "$PIVILION_E_INSTALL" -fi +pivilion_load_config export DEBIAN_FRONTEND=noninteractive -BACKUP_DIR="" -TRANSACTION_ACTIVE=0 -declare -a MANAGED_PATHS=( - /etc/pivilion.defaults /etc/pivilion.php /etc/profile.d/pivilion-motd.sh /etc/issue.d/90-pivilion.issue - /etc/apache2/ports.conf /etc/apache2/conf-available/pivilion.conf - /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/000-upload.conf - /etc/apache2/conf-enabled/pivilion.conf /etc/apache2/sites-enabled/000-default.conf - /etc/tor/torrc /etc/tor/torrc.d/pivilion.conf - /etc/systemd/system/pivilion-mode.service /etc/systemd/system/pivilion-dnsmasq.service - /etc/systemd/system/pivilion-hostapd.service /etc/systemd/system/pivilion-boot-greeter.service - /etc/systemd/system/pivilion-login-banner.service /etc/rc.local - /usr/local/lib/pivilion /usr/local/bin/pivilion /usr/local/bin/pivilion-diagnose - /usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey /usr/local/bin/generator - /usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess - /var/lib/pivilion/install-state - /etc/php/7.4/apache2/conf.d/99-pivilion.ini - /etc/php/8.2/apache2/conf.d/99-pivilion.ini - /etc/php/8.4/apache2/conf.d/99-pivilion.ini -) - -log() { printf '==> %s\n' "$*"; } -warn() { printf 'WARNING: %s\n' "$*" >&2; } - -preflight() { - local codename free_kb iface package - log "Running preflight checks" - [ -r /etc/os-release ] || { echo "Cannot identify the operating system." >&2; return "$PIVILION_E_INSTALL"; } - . /etc/os-release - codename=${VERSION_CODENAME:-unknown} - case "$codename" in bullseye|bookworm|trixie) ;; *) echo "Unsupported OS codename: $codename" >&2; return "$PIVILION_E_INSTALL" ;; esac - getent passwd "$PIVILION_USER" >/dev/null || return "$PIVILION_E_CONFIG" - [ -d "$PIVILION_HOME" ] || { echo "Configured home does not exist: $PIVILION_HOME" >&2; return "$PIVILION_E_CONFIG"; } - free_kb=$(df -Pk / | awk 'NR == 2 {print $4}') - [ "${free_kb:-0}" -ge 524288 ] || { echo "At least 512 MiB free space is required." >&2; return "$PIVILION_E_INSTALL"; } - iface=$(pivilion_wifi_interface) - [ -n "$iface" ] || { echo "No Wi-Fi interface found." >&2; return "$PIVILION_E_HARDWARE"; } - [ -d "/sys/class/net/$iface" ] || { echo "Configured Wi-Fi interface does not exist: $iface" >&2; return "$PIVILION_E_HARDWARE"; } - if command -v iw >/dev/null 2>&1; then - local phy - phy=$(iw dev "$iface" info 2>/dev/null | awk '$1 == "wiphy" {print "phy" $2; exit}') - [ -z "$phy" ] || iw phy "$phy" info 2>/dev/null | grep -Eq '^[[:space:]]*\* AP$' || warn "$iface does not advertise AP mode; hotspot activation will fail safely." - fi - if ss -H -ltn 'sport = :80' 2>/dev/null | grep -q .; then warn "Port 80 is currently occupied; this is expected only when Apache is already installed."; fi - if ss -H -lntu 'sport = :53' 2>/dev/null | grep -Eq '(0\.0\.0\.0|\*):53'; then warn "A wildcard DNS listener may conflict with captive mode."; fi - if command -v apt-cache >/dev/null 2>&1; then - for package in apache2 php tor dnsmasq hostapd nftables network-manager; do - apt-cache show "$package" >/dev/null 2>&1 || warn "Package metadata is unavailable for $package; apt-get update may be required." - done - fi - log "Preflight passed for $codename using interface $iface" -} is_networkmanager_system() { - command -v nmcli >/dev/null 2>&1 && { systemctl is-enabled --quiet NetworkManager 2>/dev/null || systemctl is-active --quiet NetworkManager 2>/dev/null; } + command -v nmcli >/dev/null 2>&1 && { + systemctl is-enabled --quiet NetworkManager 2>/dev/null || + systemctl is-active --quiet NetworkManager 2>/dev/null + } } install_packages() { - local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq dnsutils hostapd nftables iproute2 iw rfkill curl wget unzip wpasupplicant) - log "Installing required packages" + local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq nftables iproute2 iw rfkill wget unzip wpasupplicant) apt-get update apt-get install -y "${packages[@]}" if is_networkmanager_system; then - log "Using NetworkManager" + echo "Using the installed NetworkManager backend." elif command -v dhcpcd >/dev/null 2>&1 || dpkg-query -W dhcpcd5 >/dev/null 2>&1; then - apt-get install -y dhcpcd5 - log "Using dhcpcd with dedicated Pivilion hostapd" + apt-get install -y hostapd dhcpcd5 + echo "Using the installed dhcpcd/hostapd backend." else apt-get install -y network-manager systemctl enable NetworkManager - log "Installed NetworkManager because no supported backend was active" + echo "No active networking backend was found; installed NetworkManager." fi } -backup_managed_files() { - local path relative - install -d -m 0700 /var/backups/pivilion - BACKUP_DIR=$(mktemp -d /var/backups/pivilion/install-$(date +%Y%m%d-%H%M%S).XXXXXX) - install -d -m 0700 "$BACKUP_DIR/files" - : > "$BACKUP_DIR/present"; : > "$BACKUP_DIR/missing"; : > "$BACKUP_DIR/enabled-services"; : > "$BACKUP_DIR/active-services" - for service in apache2 pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do - systemctl is-enabled --quiet "$service" 2>/dev/null && printf '%s\n' "$service" >> "$BACKUP_DIR/enabled-services" || true - systemctl is-active --quiet "$service" 2>/dev/null && printf '%s\n' "$service" >> "$BACKUP_DIR/active-services" || true - done - for path in "${MANAGED_PATHS[@]}"; do - relative=${path#/} - if [ -e "$path" ] || [ -L "$path" ]; then - printf '%s\n' "$path" >> "$BACKUP_DIR/present" - install -d -m 0700 "$BACKUP_DIR/files/$(dirname "$relative")" - cp -a "$path" "$BACKUP_DIR/files/$relative" - else - printf '%s\n' "$path" >> "$BACKUP_DIR/missing" - fi - done - TRANSACTION_ACTIVE=1 - log "Configuration backup created at $BACKUP_DIR" -} - -rollback() { - local rc=$? path relative service - trap - ERR INT TERM - [ "$TRANSACTION_ACTIVE" -eq 1 ] || exit "$rc" - warn "Installation failed; restoring managed configuration from $BACKUP_DIR" - while IFS= read -r path; do [ -n "$path" ] && rm -rf -- "$path"; done < "$BACKUP_DIR/missing" - while IFS= read -r path; do - [ -n "$path" ] || continue - relative=${path#/}; rm -rf -- "$path"; install -d -m 0755 "$(dirname "$path")"; cp -a "$BACKUP_DIR/files/$relative" "$path" - done < "$BACKUP_DIR/present" - systemctl daemon-reload 2>/dev/null || true - systemctl disable apache2 pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service tor.service tor@default.service 2>/dev/null || true - while IFS= read -r path; do [ -n "$path" ] && systemctl enable "$path" 2>/dev/null || true; done < "$BACKUP_DIR/enabled-services" - for service in apache2 pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do - grep -Fqx "$service" "$BACKUP_DIR/active-services" || systemctl stop "$service" 2>/dev/null || true - done - while IFS= read -r service; do [ -n "$service" ] && systemctl start "$service" 2>/dev/null || true; done < "$BACKUP_DIR/active-services" - apache2ctl configtest >/dev/null 2>&1 && systemctl reload apache2 2>/dev/null || true - warn "Rollback complete. Package installations were retained." - exit "$rc" -} -trap rollback ERR INT TERM - -atomic_from_stdin() { - local destination=$1 mode=${2:-0644} tmp - install -d -m 0755 "$(dirname "$destination")" - tmp=$(mktemp "$(dirname "$destination")/.pivilion.XXXXXX") - cat > "$tmp"; chmod "$mode" "$tmp"; mv -f "$tmp" "$destination" -} - deploy_files() { local php_version - log "Deploying Pivilion files" - install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d /etc/profile.d /var/lib/pivilion + install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d install -m 0644 "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" /usr/local/lib/pivilion/common.sh - install -m 0755 "$SOURCE_DIR/usr/local/lib/pivilion/mode.sh" "$SOURCE_DIR/usr/local/lib/pivilion/diagnose.sh" \ - "$SOURCE_DIR/usr/local/lib/pivilion/issue-banner.sh" /usr/local/lib/pivilion/ + install -m 0755 "$SOURCE_DIR/usr/local/lib/pivilion/mode.sh" /usr/local/lib/pivilion/mode.sh install -m 0755 "$SOURCE_DIR"/usr/local/bin/* /usr/local/bin/ - install -m 0644 "$SOURCE_DIR"/etc/systemd/system/*.service /etc/systemd/system/ + install -m 0644 "$SOURCE_DIR/etc/systemd/system/pivilion-mode.service" /etc/systemd/system/pivilion-mode.service + install -m 0644 "$SOURCE_DIR/etc/systemd/system/pivilion-dnsmasq.service" /etc/systemd/system/pivilion-dnsmasq.service install -m 0644 "$SOURCE_DIR/etc/tor/torrc.d/pivilion.conf" /etc/tor/torrc.d/pivilion.conf - install -m 0644 "$SOURCE_DIR/etc/profile.d/pivilion-motd.sh" /etc/profile.d/pivilion-motd.sh - atomic_from_stdin /etc/pivilion.defaults < /etc/pivilion.defaults < /etc/pivilion.php </dev/null || true - rm -f /etc/systemd/system/pivilion-boot-greeter.service /usr/local/lib/pivilion/boot-greeter.sh - - [ -f /etc/rc.local ] || return 0 - grep -q 'hostapd -B' /etc/rc.local && signatures=$((signatures + 1)) || true - grep -q 'ifconfig br0' /etc/rc.local && signatures=$((signatures + 1)) || true - grep -q 'iptables -t nat' /etc/rc.local && signatures=$((signatures + 1)) || true - grep -q 'service dnsmasq start' /etc/rc.local && signatures=$((signatures + 1)) || true - grep -q '/usr/local/lib/pivilion/mode.sh' /etc/rc.local && signatures=4 || true - if [ "$signatures" -ge 4 ]; then - cp -a /etc/rc.local "$BACKUP_DIR/legacy-rc.local" - atomic_from_stdin /etc/rc.local 0755 <<'EOF' -#!/bin/sh -e -# Pivilion startup is managed by pivilion-mode.service. -exit 0 -EOF - log "Migrated legacy Pivilion rc.local; original saved in $BACKUP_DIR/legacy-rc.local" - elif grep -q '/var/lib/tor/hidden_service/hostname.*home/pi/torname' /etc/rc.local; then - cp -a /etc/rc.local "$BACKUP_DIR/legacy-rc.local" - if grep -Fq '_IP=$(hostname -I)' /etc/rc.local && grep -Fq 'My IP address is' /etc/rc.local; then - atomic_from_stdin /etc/rc.local 0755 <<'EOF' -#!/bin/sh -e -# Pivilion startup and the TTY banner are managed by systemd and agetty. -exit 0 -EOF - else - tmp=$(mktemp) - grep -v '/var/lib/tor/hidden_service/hostname.*home/pi/torname' /etc/rc.local > "$tmp" - install -m 0755 "$tmp" /etc/rc.local - rm -f "$tmp" - fi - log "Migrated legacy Pivilion onion rc.local; original saved in $BACKUP_DIR/legacy-rc.local" - fi -} - configure_services() { - local tmp - log "Configuring services" - tmp=$(mktemp); sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-default.conf" > "$tmp"; install -m 0644 "$tmp" /etc/apache2/sites-available/000-default.conf; rm -f "$tmp" - tmp=$(mktemp); sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-upload.conf" > "$tmp"; install -m 0644 "$tmp" /etc/apache2/sites-available/000-upload.conf; rm -f "$tmp" + sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-default.conf" > /etc/apache2/sites-available/000-default.conf + sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-upload.conf" > /etc/apache2/sites-available/000-upload.conf install -m 0644 "$SOURCE_DIR/etc/apache2/ports.conf" /etc/apache2/ports.conf - atomic_from_stdin /etc/apache2/conf-available/pivilion.conf < /etc/apache2/conf-available/pivilion.conf < Options Indexes FollowSymLinks AllowOverride All Require all granted EOF - a2enmod rewrite; a2enconf pivilion; a2ensite 000-default.conf + a2enmod rewrite + a2enconf pivilion + a2ensite 000-default.conf + if ! grep -Fqx '%include /etc/tor/torrc.d/*.conf' /etc/tor/torrc; then - tmp=$(mktemp); cp /etc/tor/torrc "$tmp"; printf '\n%%include /etc/tor/torrc.d/*.conf\n' >> "$tmp"; install -m 0644 "$tmp" /etc/tor/torrc; rm -f "$tmp" + printf '\n%%include /etc/tor/torrc.d/*.conf\n' >> /etc/tor/torrc fi - migrate_legacy_console_greeter systemctl daemon-reload - systemctl enable apache2 pivilion-mode.service pivilion-login-banner.service + systemctl enable apache2 pivilion-mode.service systemctl enable tor@default.service 2>/dev/null || systemctl enable tor.service - systemctl disable pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true - /usr/local/lib/pivilion/issue-banner.sh - command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true + systemctl disable --now dnsmasq 2>/dev/null || true + systemctl disable pivilion-dnsmasq.service 2>/dev/null || true } validate_install() { - local failed=0 - log "Validating installation" - bash -n /usr/local/lib/pivilion/*.sh /usr/local/bin/pivilion /usr/local/bin/pivilion-diagnose /usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey /usr/local/bin/generator /usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess || failed=1 - apache2ctl configtest || failed=1 - tor --verify-config -f /etc/tor/torrc || failed=1 - dnsmasq --test || failed=1 - php -l "$PIVILION_WEBROOT/gen.php" || failed=1 - systemd-analyze verify /etc/systemd/system/pivilion-*.service || failed=1 - grep -q '^My IP address is ' /etc/issue.d/90-pivilion.issue || failed=1 - grep -q '^@@@@@@@@@@@@@@@@@@@' /etc/issue.d/90-pivilion.issue || failed=1 - grep -q 'StandardOutput=null' /etc/systemd/system/pivilion-login-banner.service || failed=1 - if command -v shellcheck >/dev/null 2>&1; then shellcheck /usr/local/lib/pivilion/*.sh /usr/local/bin/pivilion* /usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey || failed=1; fi - [ "$failed" -eq 0 ] || return "$PIVILION_E_INSTALL" + apache2ctl configtest + tor --verify-config -f /etc/tor/torrc + dnsmasq --test + bash -n /usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/bin/pivilion \ + /usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey /usr/local/bin/generator \ + /usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess } -write_install_state() { - atomic_from_stdin /var/lib/pivilion/install-state <&2; failed=1; fi - done - if [ "$failed" -eq 0 ]; then validate_install || failed=1; fi - if [ "$failed" -eq 0 ]; then log "Installed files and service configuration are valid"; return 0; fi - echo "Installation check failed. Run sudo ./install.sh --repair" >&2 - return "$PIVILION_E_INSTALL" -} - -preflight -case "$ACTION" in - dry-run) - log "Dry run complete; installation would install packages, back up managed configuration, deploy files, validate, and enable services." - exit 0 ;; - check) check_install; exit $? ;; -esac - -backup_managed_files install_packages deploy_files configure_services validate_install -write_install_state -systemctl is-active --quiet apache2 2>/dev/null && systemctl reload apache2 || true -TRANSACTION_ACTIVE=0 -trap - ERR INT TERM -log "Pivilion $ACTION completed for $PIVILION_USER. Reboot to enter $(pivilion_current_mode) mode." +echo "Pivilion installed for $PIVILION_USER. Reboot to enter $(pivilion_current_mode) mode." diff --git a/tests/run.sh b/tests/run.sh deleted file mode 100755 index fa1fadb..0000000 --- a/tests/run.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -failures=0 - -run() { - local label=$1 - shift - if "$@"; then printf '[pass] %s\n' "$label"; else printf '[fail] %s\n' "$label" >&2; failures=$((failures + 1)); fi -} - -shell_syntax() { - bash -n "$ROOT/install.sh" "$ROOT"/usr/local/lib/pivilion/*.sh "$ROOT"/usr/local/bin/* "$ROOT/etc/profile.d/pivilion-motd.sh" -} - -php_syntax() { - command -v php >/dev/null 2>&1 || return 0 - while IFS= read -r file; do php -l "$file" >/dev/null || return 1; done < <(find "$ROOT/var" "$ROOT/home" -name '*.php' -type f) -} - -systemd_syntax() { - local output - command -v systemd-analyze >/dev/null 2>&1 || return 0 - output=$(systemd-analyze verify "$ROOT"/etc/systemd/system/*.service 2>&1) || true - output=$(printf '%s\n' "$output" | grep -Ev 'SO_PASS(RIGHTS|CRED)|handoff timestamp|user lookup socket' || true) - [ -z "$output" ] -} - -captive_rules() { - grep -q 'R=302' "$ROOT/home/pi/pivilion/config/html/.htaccess" && - ! grep -q 'R=301' "$ROOT/home/pi/pivilion/config/html/.htaccess" && - ! grep -Eq 'dport[[:space:]]+443|redirect to :443' "$ROOT/usr/local/lib/pivilion/mode.sh" -} - -installer_safety() { - grep -q -- '--dry-run' "$ROOT/install.sh" && - grep -q -- '--repair' "$ROOT/install.sh" && - grep -q 'backup_managed_files' "$ROOT/install.sh" && - grep -q 'rollback()' "$ROOT/install.sh" && - ! grep -Eq 'disable( --now)? dnsmasq' "$ROOT/install.sh" && - ! grep -q 'rm -f.*CAPTIVE\|rm -f.*\.htaccess' "$ROOT/usr/local/bin/onion" -} - -state_contract() { - grep -q "printf 'desired_mode" "$ROOT/usr/local/lib/pivilion/common.sh" && - grep -q "printf 'active_mode" "$ROOT/usr/local/lib/pivilion/common.sh" && - grep -q "printf 'health" "$ROOT/usr/local/lib/pivilion/common.sh" && - grep -q 'degraded' "$ROOT/usr/local/lib/pivilion/mode.sh" -} - -tty_banner_contract() { - grep -q 'StandardOutput=null' "$ROOT/etc/systemd/system/pivilion-login-banner.service" && - ! grep -q 'journal+console' "$ROOT/etc/systemd/system/pivilion-login-banner.service" && - grep -q '/etc/issue.d/90-pivilion.issue' "$ROOT/usr/local/lib/pivilion/issue-banner.sh" && - grep -q 'SSH_CONNECTION' "$ROOT/etc/profile.d/pivilion-motd.sh" && - grep -q 'agetty --reload' "$ROOT/usr/local/lib/pivilion/mode.sh" && - ! grep -Eq '(^|[[:space:]])/etc/issue([[:space:]]|$)' "$ROOT/install.sh" -} - -shellcheck_scripts() { - command -v shellcheck >/dev/null 2>&1 || return 0 - shellcheck "$ROOT/install.sh" "$ROOT"/usr/local/lib/pivilion/*.sh "$ROOT"/usr/local/bin/pivilion* "$ROOT/usr/local/bin/hotspot" "$ROOT/usr/local/bin/onion" "$ROOT/usr/local/bin/pikey" -} - -run "configuration and state fixtures" "$ROOT/tests/test-common.sh" -run "shell syntax" shell_syntax -run "PHP syntax" php_syntax -run "systemd unit syntax" systemd_syntax -run "temporary HTTP-only captive rules" captive_rules -run "transactional installer safety contract" installer_safety -run "truthful runtime state contract" state_contract -run "agetty login-banner contract" tty_banner_contract -run "ShellCheck (when installed)" shellcheck_scripts - -[ "$failures" -eq 0 ] || { echo "$failures test group(s) failed." >&2; exit 1; } -echo "All available tests passed." diff --git a/tests/test-common.sh b/tests/test-common.sh deleted file mode 100755 index bb5cd9f..0000000 --- a/tests/test-common.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -USER_NAME=$(id -un) - -cat > "$TMP/primary.conf" < "$TMP/fallback.conf" < "$PIVILION_DIR/mode" -[ "$(pivilion_current_mode)" = onion ] -cat > "$PIVILION_STATE_FILE" < "$TMP/pivilion.issue" -grep -Fqx 'My IP address is \4{wlan-test}' "$TMP/pivilion.issue" -grep -q '^Welcome to Pivilion!$' "$TMP/pivilion.issue" -grep -q '^@@@@@@@@@@@@@@@@@@@' "$TMP/pivilion.issue" - -mkdir "$TMP/bin" -cat > "$TMP/bin/nmcli" <<'EOF' -#!/bin/sh -exit 0 -EOF -cat > "$TMP/bin/systemctl" <<'EOF' -#!/bin/sh -[ "${MOCK_NM_ACTIVE:-0}" = 1 ] -EOF -chmod +x "$TMP/bin/nmcli" "$TMP/bin/systemctl" -PATH="$TMP/bin:$PATH" MOCK_NM_ACTIVE=1 pivilion_uses_networkmanager -if PATH="$TMP/bin:$PATH" MOCK_NM_ACTIVE=0 pivilion_uses_networkmanager; then exit 1; fi - -sed 's/PIVILION_MODE=hotspot/PIVILION_MODE=invalid/' "$TMP/primary.conf" > "$TMP/invalid.conf" -PIVILION_CONFIG_PATHS="$TMP/invalid.conf $TMP/fallback.conf" -export PIVILION_CONFIG_PATHS -if pivilion_load_config >/dev/null 2>&1; then exit 1; fi diff --git a/usr/local/bin/onion b/usr/local/bin/onion index 7d2db00..5014f28 100755 --- a/usr/local/bin/onion +++ b/usr/local/bin/onion @@ -4,6 +4,7 @@ source /usr/local/lib/pivilion/common.sh [ "$(id -u)" -eq 0 ] || exec sudo "$0" "$@" pivilion_load_config pivilion_set_mode onion +rm -f "$PIVILION_WEBROOT/gen/.htaccess" echo "Pivilion will start in onion mode after the next reboot." if [ -s "$PIVILION_DIR/torname" ]; then printf 'Your onion domain is %s\n' "$(tr -d '\r\n' < "$PIVILION_DIR/torname")" diff --git a/usr/local/bin/pivilion b/usr/local/bin/pivilion index 186ddd9..44959f1 100755 --- a/usr/local/bin/pivilion +++ b/usr/local/bin/pivilion @@ -3,22 +3,21 @@ set -euo pipefail source /usr/local/lib/pivilion/common.sh pivilion_load_config -case "${1:-status}" in - status) [ "$#" -le 1 ] || { echo "Usage: pivilion [status|diagnose [--json]]" >&2; exit 2; } ;; - diagnose) shift; exec /usr/local/lib/pivilion/diagnose.sh "$@" ;; - *) echo "Usage: pivilion [status|diagnose [--json]]" >&2; exit 2 ;; -esac - -pivilion_print_motd - -failure=$(pivilion_state_value failure none) +mode=$(pivilion_current_mode) +torname="not generated yet" +[ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname") +uptime_text=$(uptime -p 2>/dev/null || true) cat </dev/null 2>&1; then pivilion_service_state tor@default.service; else pivilion_service_state tor.service; fi) -Last activation failure: $failure +Welcome to Pivilion! + +Host...............: $(uname -n) +System.............: $(uname -srmo) +Uptime.............: ${uptime_text:-unknown} +Mode after reboot..: $mode +Onion address......: $torname +Web root...........: $PIVILION_WEBROOT Commands: pivilion show this status @@ -29,6 +28,4 @@ Commands: static convert the current gallery to static files hotglue install or restore Hotglue htaccess reset or remove captive redirect rules - pivilion diagnose [--json] check networking, redirects, and services - pivilion-diagnose compatibility alias for diagnostics EOF diff --git a/usr/local/bin/pivilion-diagnose b/usr/local/bin/pivilion-diagnose deleted file mode 100755 index f95b41c..0000000 --- a/usr/local/bin/pivilion-diagnose +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -euo pipefail -exec /usr/local/lib/pivilion/diagnose.sh "$@" diff --git a/usr/local/lib/pivilion/common.sh b/usr/local/lib/pivilion/common.sh index 4a2672b..39b26a0 100644 --- a/usr/local/lib/pivilion/common.sh +++ b/usr/local/lib/pivilion/common.sh @@ -3,18 +3,6 @@ # Shared configuration and small helpers for the Pivilion commands. PIVILION_CONFIG="" -PIVILION_STATE_DIR=${PIVILION_STATE_DIR:-/run/pivilion} -PIVILION_STATE_FILE=${PIVILION_STATE_FILE:-$PIVILION_STATE_DIR/status.tsv} - -# Stable command exit codes. Keep these public so scripts and monitoring can -# distinguish configuration, hardware, service, captive portal, and Tor faults. -PIVILION_E_CONFIG=10 -PIVILION_E_HARDWARE=11 -PIVILION_E_CONFLICT=12 -PIVILION_E_SERVICE=13 -PIVILION_E_CAPTIVE=14 -PIVILION_E_TOR=15 -PIVILION_E_INSTALL=20 _pivilion_trim() { local value=$1 @@ -24,9 +12,8 @@ _pivilion_trim() { } _pivilion_config_value() { - local key=$1 file line name value config_paths - config_paths=${PIVILION_CONFIG_PATHS:-/boot/firmware/pivilion.conf /boot/pivilion.conf /etc/pivilion.conf /etc/pivilion.defaults} - for file in $config_paths; do + local key=$1 file line name value + for file in /boot/firmware/pivilion.conf /boot/pivilion.conf /etc/pivilion.conf /etc/pivilion.defaults; do [ -r "$file" ] || continue while IFS= read -r line || [ -n "$line" ]; do line=$(_pivilion_trim "$line") @@ -124,87 +111,3 @@ pivilion_current_mode() { pivilion_require_root() { [ "$(id -u)" -eq 0 ] || { echo "This operation must run as root (use sudo)." >&2; return 1; } } - -pivilion_state_value() { - local key=$1 fallback=${2:-unknown} value - value=$(awk -F '\t' -v key="$key" '$1 == key {sub(/^[^\t]*\t/, ""); print; exit}' "$PIVILION_STATE_FILE" 2>/dev/null || true) - printf '%s\n' "${value:-$fallback}" -} - -pivilion_write_state() { - local active=$1 backend=$2 health=$3 failure=${4:-} tmp desired addresses - pivilion_require_root - install -d -m 0755 "$PIVILION_STATE_DIR" - desired=$(pivilion_current_mode) - addresses=$(hostname -I 2>/dev/null | xargs || true) - failure=${failure//$'\n'/ } - failure=${failure//$'\t'/ } - tmp=$(mktemp "$PIVILION_STATE_DIR/status.XXXXXX") - { - printf 'desired_mode\t%s\n' "$desired" - printf 'active_mode\t%s\n' "$active" - printf 'backend\t%s\n' "$backend" - printf 'health\t%s\n' "$health" - printf 'activated_at\t%s\n' "$(date --iso-8601=seconds)" - printf 'addresses\t%s\n' "${addresses:-none}" - printf 'failure\t%s\n' "${failure:-none}" - } > "$tmp" - chmod 0644 "$tmp" - mv -f "$tmp" "$PIVILION_STATE_FILE" -} - -pivilion_service_state() { - local state - state=$(systemctl is-active "$1" 2>/dev/null || true) - printf '%s\n' "${state:-unknown}" -} - -pivilion_print_motd() { - local heading=${1:-Welcome to Pivilion!} - local show_addresses=${2:-yes} - local uptime_text memory_free memory_total load_one load_five load_fifteen processes torname desired active backend health addresses - - uptime_text=$(uptime -p 2>/dev/null || printf 'unknown') - memory_free=$(awk '/^MemAvailable:/ {print $2; exit}' /proc/meminfo 2>/dev/null) - memory_total=$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null) - if ! read -r load_one load_five load_fifteen _ < /proc/loadavg 2>/dev/null; then - load_one=unknown; load_five=unknown; load_fifteen=unknown - fi - processes=$({ ps ax --no-headers 2>/dev/null || true; } | wc -l) - [ "$processes" -gt 0 ] 2>/dev/null || processes=unknown - torname=not-generated-yet - [ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname") - desired=$(pivilion_current_mode) - active=$(pivilion_state_value active_mode not-verified) - backend=$(pivilion_state_value backend unknown) - health=$(pivilion_state_value health unknown) - addresses=$(pivilion_state_value addresses none) - - printf '\n%s\n' "$heading" - cat <&2; exit 2 ;; - esac - shift -done - -pivilion_load_config || exit "$PIVILION_E_CONFIG" -DESIRED=$(pivilion_current_mode) -ACTIVE=$(pivilion_state_value active_mode not-verified) -BACKEND=$(pivilion_state_value backend unknown) -RECORDED_HEALTH=$(pivilion_state_value health unknown) -ADDRESSES=$(pivilion_state_value addresses none) -FAILURE_REASON=$(pivilion_state_value failure none) -MODE=${MODE:-$ACTIVE} -case "$MODE" in not-verified|none|unknown) MODE=$DESIRED ;; esac -case "$MODE" in hotspot|onion) ;; *) MODE=$(pivilion_current_mode) ;; esac - -declare -a CHECK_NAMES=() CHECK_VALUES=() CHECK_DETAILS=() -FAILURES=0 -SERVICE_FAILURE=0 -MODE_FAILURE=0 -HARDWARE_FAILURE=0 - -record_check() { - local name=$1 value=$2 detail=${3:-} - CHECK_NAMES+=("$name") - CHECK_VALUES+=("$value") - CHECK_DETAILS+=("$detail") - if [ "$value" != pass ]; then FAILURES=$((FAILURES + 1)); fi -} - -service_active() { systemctl is-active --quiet "$1"; } -apache_rewrite() { apache2ctl -M 2>/dev/null | grep -q rewrite_module; } -live_redirect() { cmp -s "$PIVILION_DIR/config/html/.htaccess" "$PIVILION_WEBROOT/gen/.htaccess"; } -dns_answer() { [ "$(dig +short A @10.1.1.1 "$1" 2>/dev/null | tail -n1)" = 10.1.1.1 ]; } -nft_active() { nft list table inet pivilion >/dev/null 2>&1; } -probe_redirect() { - local host=$1 path=$2 headers - headers=$(curl -sS --max-time 5 -D - -o /dev/null -H "Host: $host" "http://10.1.1.1$path") || return 1 - grep -Eq '^HTTP/[0-9.]+ 302([[:space:]]|$)' <<<"$headers" && - grep -Eiq '^Location:[[:space:]]*http://10\.1\.1\.1/index\.php\r?$' <<<"$headers" -} -gallery_works() { curl -fsS --max-time 5 -o /dev/null http://10.1.1.1/index.php; } -default_route() { ip route show default | grep -q '^default'; } -valid_torname() { - local hostname - hostname=$(tr -d '\r\n' < "$PIVILION_DIR/torname" 2>/dev/null || true) - [[ "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]] -} -tor_active() { - if systemctl cat tor@default.service >/dev/null 2>&1; then service_active tor@default.service; else service_active tor.service; fi -} - -IFACE=$(pivilion_wifi_interface) -if [ -n "$IFACE" ] && [ -d "/sys/class/net/$IFACE" ]; then record_check wifi_interface pass "$IFACE"; else record_check wifi_interface fail missing; HARDWARE_FAILURE=1; fi -if service_active apache2; then record_check apache pass active; else record_check apache fail inactive; SERVICE_FAILURE=1; fi -if apache_rewrite; then record_check apache_rewrite pass enabled; else record_check apache_rewrite fail disabled; SERVICE_FAILURE=1; fi - -if [ "$MODE" = hotspot ]; then - if live_redirect; then record_check captive_redirect pass installed; else record_check captive_redirect fail missing_or_stale; MODE_FAILURE=1; fi - if service_active pivilion-dnsmasq.service; then record_check dnsmasq pass active; else record_check dnsmasq fail inactive; SERVICE_FAILURE=1; fi - if nft_active; then record_check nftables pass active; else record_check nftables fail missing; MODE_FAILURE=1; fi - if dns_answer connectivitycheck.gstatic.com; then record_check captive_dns pass 10.1.1.1; else record_check captive_dns fail wrong_answer; MODE_FAILURE=1; fi - if probe_redirect connectivitycheck.gstatic.com /generate_204; then record_check android_probe pass 302; else record_check android_probe fail no_redirect; MODE_FAILURE=1; fi - if probe_redirect captive.apple.com /hotspot-detect.html; then record_check apple_probe pass 302; else record_check apple_probe fail no_redirect; MODE_FAILURE=1; fi - if probe_redirect www.msftconnecttest.com /connecttest.txt; then record_check microsoft_probe pass 302; else record_check microsoft_probe fail no_redirect; MODE_FAILURE=1; fi - if gallery_works; then record_check gallery pass reachable; else record_check gallery fail unreachable; MODE_FAILURE=1; fi -else - if default_route; then record_check upstream_route pass present; else record_check upstream_route fail missing; MODE_FAILURE=1; fi - if tor_active; then record_check tor pass active; else record_check tor fail inactive; MODE_FAILURE=1; fi - if valid_torname; then record_check onion_hostname pass valid_v3; else record_check onion_hostname fail missing_or_invalid; MODE_FAILURE=1; fi -fi - -json_escape() { - local value=$1 - value=${value//\\/\\\\}; value=${value//\"/\\\"}; value=${value//$'\n'/\\n}; value=${value//$'\r'/\\r}; value=${value//$'\t'/\\t} - printf '%s' "$value" -} - -if [ "$OUTPUT" = json ]; then - printf '{"desired_mode":"%s","active_mode":"%s","diagnosed_mode":"%s","backend":"%s","recorded_health":"%s","addresses":"%s","failure":"%s","healthy":%s,"checks":{' \ - "$(json_escape "$DESIRED")" "$(json_escape "$ACTIVE")" "$(json_escape "$MODE")" "$(json_escape "$BACKEND")" \ - "$(json_escape "$RECORDED_HEALTH")" "$(json_escape "$ADDRESSES")" "$(json_escape "$FAILURE_REASON")" \ - "$([ "$FAILURES" -eq 0 ] && printf true || printf false)" - for ((i=0; i<${#CHECK_NAMES[@]}; i++)); do - [ "$i" -eq 0 ] || printf ',' - printf '"%s":{"status":"%s","detail":"%s"}' "$(json_escape "${CHECK_NAMES[$i]}")" "${CHECK_VALUES[$i]}" "$(json_escape "${CHECK_DETAILS[$i]}")" - done - printf '}}\n' -elif [ "$QUIET" -eq 0 ]; then - printf 'Pivilion diagnostics (desired=%s, active=%s, checking=%s, backend=%s, recorded-health=%s):\n' \ - "$DESIRED" "$ACTIVE" "$MODE" "$BACKEND" "$RECORDED_HEALTH" - for ((i=0; i<${#CHECK_NAMES[@]}; i++)); do - printf ' [%s] %-20s %s\n' "${CHECK_VALUES[$i]}" "${CHECK_NAMES[$i]}" "${CHECK_DETAILS[$i]}" - done -fi - -[ "$FAILURES" -eq 0 ] && exit 0 -[ "$HARDWARE_FAILURE" -eq 0 ] || exit "$PIVILION_E_HARDWARE" -[ "$SERVICE_FAILURE" -eq 0 ] || exit "$PIVILION_E_SERVICE" -[ "$MODE" != hotspot ] || exit "$PIVILION_E_CAPTIVE" -exit "$PIVILION_E_TOR" diff --git a/usr/local/lib/pivilion/issue-banner.sh b/usr/local/lib/pivilion/issue-banner.sh deleted file mode 100755 index 5e1a0e3..0000000 --- a/usr/local/lib/pivilion/issue-banner.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -euo pipefail - -source /usr/local/lib/pivilion/common.sh -pivilion_require_root -pivilion_load_config - -ISSUE_FILE=/etc/issue.d/90-pivilion.issue -ISSUE_DIR=$(dirname "$ISSUE_FILE") -install -d -m 0755 "$ISSUE_DIR" -tmp=$(mktemp "$ISSUE_DIR/.90-pivilion.XXXXXX") -trap 'rm -f "$tmp"' EXIT -pivilion_print_issue > "$tmp" -chmod 0644 "$tmp" -mv -f "$tmp" "$ISSUE_FILE" -trap - EXIT diff --git a/usr/local/lib/pivilion/mode.sh b/usr/local/lib/pivilion/mode.sh index 5c565a2..597b8c6 100755 --- a/usr/local/lib/pivilion/mode.sh +++ b/usr/local/lib/pivilion/mode.sh @@ -1,105 +1,33 @@ #!/bin/bash -set -Eeuo pipefail +set -euo pipefail source /usr/local/lib/pivilion/common.sh pivilion_require_root -pivilion_load_config || exit "$PIVILION_E_CONFIG" +pivilion_load_config MODE=${1:-$(pivilion_current_mode)} -case "$MODE" in hotspot|onion) ;; *) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;; esac IFACE=$(pivilion_wifi_interface) -[ -n "$IFACE" ] && [ -d "/sys/class/net/$IFACE" ] || { - pivilion_write_state none none failed "Wi-Fi interface not found" - echo "No Wi-Fi interface found; set PIVILION_WIFI_INTERFACE" >&2 - exit "$PIVILION_E_HARDWARE" -} - +[ -n "$IFACE" ] || { echo "No Wi-Fi interface found; set PIVILION_WIFI_INTERFACE" >&2; exit 1; } RUN_DIR=/run/pivilion DNSMASQ_CONF=/etc/dnsmasq.d/pivilion-runtime.conf -HOSTAPD_CONF=$RUN_DIR/hostapd.conf -NFT_CONF=$RUN_DIR/pivilion.nft NM_HOTSPOT=pivilion-hotspot -CAPTIVE_TEMPLATE=$PIVILION_DIR/config/html/.htaccess -CAPTIVE_LIVE=$PIVILION_WEBROOT/gen/.htaccess -BACKEND=legacy -pivilion_uses_networkmanager && BACKEND=networkmanager -refresh_login_banner() { - if [ -x /usr/local/lib/pivilion/issue-banner.sh ]; then - /usr/local/lib/pivilion/issue-banner.sh || logger -t pivilion-mode "Could not refresh TTY login banner" - fi - command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true -} - -activation_error() { - local rc=$? line=${BASH_LINENO[0]:-unknown} - trap - ERR - pivilion_write_state none "$BACKEND" failed "Activation failed at line $line (exit $rc)" || true - refresh_login_banner || true - logger -t pivilion-mode "Failed to activate $MODE mode at line $line (exit $rc)" 2>/dev/null || true - exit "$rc" -} -trap activation_error ERR - -atomic_install() { - local source=$1 destination=$2 owner=${3:-root} group=${4:-root} mode=${5:-0644} tmp - install -d -m 0755 "$(dirname "$destination")" - tmp=$(mktemp "$(dirname "$destination")/.pivilion.XXXXXX") - if ! install -m "$mode" -o "$owner" -g "$group" "$source" "$tmp"; then rm -f "$tmp"; return 1; fi - mv -f "$tmp" "$destination" -} - -install_captive_redirect() { - [ -r "$CAPTIVE_TEMPLATE" ] || { echo "Captive redirect template is missing: $CAPTIVE_TEMPLATE" >&2; return 1; } - [ -d "$PIVILION_WEBROOT/gen" ] || { echo "Gallery webroot is missing: $PIVILION_WEBROOT/gen" >&2; return 1; } - atomic_install "$CAPTIVE_TEMPLATE" "$CAPTIVE_LIVE" root www-data 0644 -} - -validate_ap_support() { - local phy - command -v iw >/dev/null 2>&1 || return 1 - phy=$(iw dev "$IFACE" info 2>/dev/null | awk '$1 == "wiphy" {print "phy" $2; exit}') - [ -n "$phy" ] && iw phy "$phy" info 2>/dev/null | grep -Eq '^[[:space:]]*\* AP$' -} - -cleanup_hotspot() { - systemctl stop pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true - rm -f "$DNSMASQ_CONF" "$HOSTAPD_CONF" +stop_hotspot() { + systemctl stop pivilion-dnsmasq.service 2>/dev/null || true + rm -f "$DNSMASQ_CONF" nft delete table inet pivilion 2>/dev/null || true - if command -v nmcli >/dev/null 2>&1; then - nmcli connection down "$NM_HOTSPOT" >/dev/null 2>&1 || true - nmcli connection delete "$NM_HOTSPOT" >/dev/null 2>&1 || true - fi - if ip -4 address show dev "$IFACE" | grep -q '10\.1\.1\.1/24'; then - ip address del 10.1.1.1/24 dev "$IFACE" 2>/dev/null || true - fi - if systemctl is-active --quiet hostapd.service 2>/dev/null && grep -Fqx "ssid=$PIVILION_HOTSPOT_SSID" /etc/hostapd/hostapd.conf 2>/dev/null; then - systemctl stop hostapd.service + if pivilion_uses_networkmanager; then + nmcli connection down "$NM_HOTSPOT" 2>/dev/null || true + nmcli connection delete "$NM_HOTSPOT" 2>/dev/null || true + else + systemctl stop hostapd 2>/dev/null || true + ip address flush dev "$IFACE" 2>/dev/null || true fi } -check_dns_conflict() { - local listeners - listeners=$(ss -H -lntu 'sport = :53' 2>/dev/null || true) - [ -z "$listeners" ] && return 0 - if grep -Eq '(0\.0\.0\.0|\*|10\.1\.1\.1):53([[:space:]]|$)' <<<"$listeners"; then - echo "Port 53 is already occupied by a non-Pivilion service." >&2 - return 1 - fi -} - -check_hostapd_conflict() { - if systemctl is-active --quiet hostapd.service 2>/dev/null; then - echo "The system hostapd service already owns a wireless interface." >&2 - return 1 - fi -} - -write_hotspot_configs() { - local dns_tmp hostapd_tmp nft_tmp - install -d -m 0755 "$RUN_DIR" /etc/dnsmasq.d - dns_tmp=$(mktemp "$RUN_DIR/dnsmasq.XXXXXX") - cat > "$dns_tmp" < "$DNSMASQ_CONF" < "$hostapd_tmp" < "$nft_tmp" < "$RUN_DIR/pivilion.nft" </dev/null || true - nft -f "$NFT_CONF" + nft -f "$RUN_DIR/pivilion.nft" } -wait_for_default_route() { - local i - for i in $(seq 1 30); do ip route show default | grep -q '^default' && return 0; sleep 1; done - return 1 -} - -restart_tor() { - if systemctl cat tor@default.service >/dev/null 2>&1; then systemctl restart tor@default.service; else systemctl restart tor.service; fi +start_hotspot() { + stop_hotspot + rfkill unblock wifi 2>/dev/null || true + systemctl start apache2 + if pivilion_uses_networkmanager; then + nmcli connection add type wifi ifname "$IFACE" con-name "$NM_HOTSPOT" ssid "$PIVILION_HOTSPOT_SSID" + nmcli connection modify "$NM_HOTSPOT" 802-11-wireless.mode ap 802-11-wireless.band bg \ + ipv4.method manual ipv4.addresses 10.1.1.1/24 ipv4.never-default yes ipv6.method disabled + nmcli connection up "$NM_HOTSPOT" + else + systemctl stop "wpa_supplicant@$IFACE.service" 2>/dev/null || true + command -v dhcpcd >/dev/null 2>&1 && dhcpcd -k "$IFACE" 2>/dev/null || true + ip link set "$IFACE" down + ip address flush dev "$IFACE" + ip address add 10.1.1.1/24 dev "$IFACE" + ip link set "$IFACE" up + install -d -m 0755 /etc/hostapd + cat > /etc/hostapd/hostapd.conf </dev/null || true + systemctl restart hostapd + fi + write_dnsmasq_config + dnsmasq --test --conf-file="$DNSMASQ_CONF" + systemctl restart pivilion-dnsmasq.service + start_firewall + echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE (10.1.1.1)." } update_torname() { @@ -161,86 +95,44 @@ update_torname() { for i in $(seq 1 30); do if [ -s "$source" ]; then hostname=$(tr -d '\r\n' < "$source") - [[ "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]] || return 1 + if [[ ! "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]]; then + echo "Tor produced an invalid v3 onion hostname." >&2 + return 1 + fi tmp=$(mktemp "$PIVILION_DIR/.torname.XXXXXX") printf '%s\n' "$hostname" > "$tmp" - chown "$PIVILION_USER:$PIVILION_GROUP" "$tmp"; chmod 0644 "$tmp"; mv -f "$tmp" "$PIVILION_DIR/torname" + chown "$PIVILION_USER:$PIVILION_GROUP" "$tmp" + chmod 0644 "$tmp" + mv -f "$tmp" "$PIVILION_DIR/torname" return 0 fi sleep 1 done + echo "Tor started, but no onion hostname was generated within 30 seconds." >&2 return 1 } -start_hotspot() { - pivilion_write_state none "$BACKEND" activating none - validate_ap_support || { - pivilion_write_state none "$BACKEND" failed "$IFACE does not advertise Wi-Fi AP support" - refresh_login_banner - echo "$IFACE does not advertise Wi-Fi AP support." >&2 - exit "$PIVILION_E_HARDWARE" - } - cleanup_hotspot - if ! check_hostapd_conflict; then - pivilion_write_state none "$BACKEND" failed "System hostapd conflict" - refresh_login_banner - exit "$PIVILION_E_CONFLICT" - fi - if ! check_dns_conflict; then - pivilion_write_state none "$BACKEND" failed "Port 53 conflict" - refresh_login_banner - exit "$PIVILION_E_CONFLICT" - fi - install_captive_redirect - write_hotspot_configs - rfkill unblock wifi 2>/dev/null || true - systemctl start apache2 - if [ "$BACKEND" = networkmanager ]; then - nmcli connection add type wifi ifname "$IFACE" con-name "$NM_HOTSPOT" ssid "$PIVILION_HOTSPOT_SSID" - nmcli connection modify "$NM_HOTSPOT" 802-11-wireless.mode ap 802-11-wireless.band bg \ - ipv4.method manual ipv4.addresses 10.1.1.1/24 ipv4.never-default yes ipv6.method disabled connection.autoconnect no - nmcli connection up "$NM_HOTSPOT" - else - command -v dhcpcd >/dev/null 2>&1 && dhcpcd -k "$IFACE" 2>/dev/null || true - systemctl stop "wpa_supplicant@$IFACE.service" 2>/dev/null || true - ip link set "$IFACE" down; ip address flush dev "$IFACE"; ip address add 10.1.1.1/24 dev "$IFACE"; ip link set "$IFACE" up - systemctl restart pivilion-hostapd.service - fi - systemctl restart pivilion-dnsmasq.service - apply_firewall - if /usr/local/lib/pivilion/diagnose.sh --mode hotspot --quiet; then - pivilion_write_state hotspot "$BACKEND" healthy none - else - pivilion_write_state hotspot "$BACKEND" degraded "Post-start captive portal diagnostics failed" - logger -t pivilion-mode "Hotspot active but degraded; run pivilion diagnose" || true - fi - refresh_login_banner - echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE." -} - start_onion() { - local failure="" - pivilion_write_state none "$BACKEND" activating none - cleanup_hotspot - rm -f "$CAPTIVE_LIVE" - if [ "$BACKEND" = networkmanager ]; then - nmcli connection up pivilion-onion >/dev/null 2>&1 || failure="No saved Pivilion Wi-Fi connection; run pikey" + stop_hotspot + if pivilion_uses_networkmanager; then + nmcli connection up pivilion-onion 2>/dev/null || echo "No saved Pivilion Wi-Fi connection; run pikey." >&2 else systemctl restart dhcpcd 2>/dev/null || true systemctl restart "wpa_supplicant@$IFACE.service" 2>/dev/null || systemctl restart wpa_supplicant 2>/dev/null || true fi - wait_for_default_route || failure="${failure:+$failure; }No upstream default route after 30 seconds" + rm -f "$PIVILION_WEBROOT/gen/.htaccess" systemctl start apache2 - restart_tor - update_torname || failure="${failure:+$failure; }Tor did not produce a valid hostname; retained previous value" - if [ -z "$failure" ] && /usr/local/lib/pivilion/diagnose.sh --mode onion --quiet; then - pivilion_write_state onion "$BACKEND" healthy none + if systemctl cat tor@default.service >/dev/null 2>&1; then + systemctl restart tor@default.service else - pivilion_write_state onion "$BACKEND" degraded "${failure:-Onion diagnostics failed}" - logger -t pivilion-mode "Onion mode active but degraded: ${failure:-diagnostics failed}" || true + systemctl restart tor.service fi - refresh_login_banner - echo "Pivilion onion mode activation completed." + update_torname || true + echo "Pivilion onion mode is active." } -case "$MODE" in hotspot) start_hotspot ;; onion) start_onion ;; esac +case "$MODE" in + hotspot) start_hotspot ;; + onion) start_onion ;; + *) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;; +esac