#!/bin/bash set -Eeuo 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; } 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 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; } } 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" apt-get update apt-get install -y "${packages[@]}" if is_networkmanager_system; then log "Using NetworkManager" 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" else apt-get install -y network-manager systemctl enable NetworkManager log "Installed NetworkManager because no supported backend was active" 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 -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/bin/* /usr/local/bin/ install -m 0644 "$SOURCE_DIR"/etc/systemd/system/*.service /etc/systemd/system/ 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 </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" install -m 0644 "$SOURCE_DIR/etc/apache2/ports.conf" /etc/apache2/ports.conf atomic_from_stdin /etc/apache2/conf-available/pivilion.conf < Options Indexes FollowSymLinks AllowOverride All Require all granted EOF 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" fi migrate_legacy_console_greeter systemctl daemon-reload systemctl enable apache2 pivilion-mode.service pivilion-login-banner.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 } 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" } 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."