diff --git a/README.md b/README.md index 97c7a40..ceb29da 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,7 @@ 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. diff --git a/etc/profile.d/pivilion-motd.sh b/etc/profile.d/pivilion-motd.sh new file mode 100644 index 0000000..fabd70d --- /dev/null +++ b/etc/profile.d/pivilion-motd.sh @@ -0,0 +1,12 @@ +# Show the Pivilion status banner for the configured owner on interactive Bash +# login shells. Other users and non-interactive sessions remain unchanged. +[ -n "${BASH_VERSION:-}" ] || return 0 +case $- in *i*) ;; *) return 0 ;; esac +[ -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/home/pi/.bashrc b/home/pi/.bashrc index 6700e9e..8f4a3be 100644 --- a/home/pi/.bashrc +++ b/home/pi/.bashrc @@ -112,27 +112,13 @@ if ! shopt -oq posix; then 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 -" +# 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 export LC_ALL=C - diff --git a/home/pi/pivilion/config/html/.htaccess b/home/pi/pivilion/config/html/.htaccess index 0e39710..1a1eb29 100644 --- a/home/pi/pivilion/config/html/.htaccess +++ b/home/pi/pivilion/config/html/.htaccess @@ -1,17 +1,11 @@ RewriteEngine on -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] + +# 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] diff --git a/install.sh b/install.sh index 03f7984..9d4e0fc 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,16 @@ #!/bin/bash set -euo pipefail +SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" +pivilion_load_config +pivilion_print_motd "Pivilion installer" + 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 - export DEBIAN_FRONTEND=noninteractive is_networkmanager_system() { @@ -20,7 +21,7 @@ is_networkmanager_system() { } install_packages() { - local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq nftables iproute2 iw rfkill wget unzip wpasupplicant) + local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq dnsutils nftables iproute2 iw rfkill curl wget unzip wpasupplicant) apt-get update apt-get install -y "${packages[@]}" if is_networkmanager_system; then @@ -37,13 +38,14 @@ install_packages() { deploy_files() { local php_version - install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d + install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d /etc/profile.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" /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/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 cat > /etc/pivilion.defaults </dev/null || true) +pivilion_print_motd cat <&2; return 1; } } + +pivilion_print_motd() { + local heading=${1:-Welcome to Pivilion!} + local uptime_text memory_free memory_total load_one load_five load_fifteen processes torname mode + + 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) + read -r load_one load_five load_fifteen _ < /proc/loadavg + processes=$(ps ax --no-headers 2>/dev/null | wc -l) + torname=not-generated-yet + [ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname") + mode=$(pivilion_current_mode) + + printf '\n%s\n' "$heading" + cat <&2 + return 1 + } + [ -d "$PIVILION_WEBROOT/gen" ] || { + echo "Gallery webroot is missing: $PIVILION_WEBROOT/gen" >&2 + return 1 + } + tmp=$(mktemp "$PIVILION_WEBROOT/gen/.htaccess.XXXXXX") + if ! install -m 0644 -o root -g www-data "$CAPTIVE_TEMPLATE" "$tmp"; then + rm -f "$tmp" + return 1 + fi + mv -f "$tmp" "$CAPTIVE_LIVE" +} + +diagnostic_result() { + local label=$1 + shift + if "$@"; then + printf ' [ok] %s\n' "$label" + return 0 + fi + printf ' [fail] %s\n' "$label" >&2 + return 1 +} + +probe_redirect() { + local host=$1 path=$2 headers + headers=$(curl --silent --show-error --max-time 5 --dump-header - --output /dev/null \ + --header "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" +} + +dns_points_to_portal() { + local host=$1 answer + answer=$(dig +short A @10.1.1.1 "$host" 2>/dev/null | tail -n1) + [ "$answer" = 10.1.1.1 ] +} + +apache_rewrite_enabled() { + apache2ctl -M 2>/dev/null | grep -q 'rewrite_module' +} + +nft_portal_table_active() { + nft list table inet pivilion >/dev/null 2>&1 +} + +hotspot_diagnostics() { + local failures=0 + echo "Pivilion hotspot diagnostics:" + diagnostic_result "Apache is active" systemctl is-active --quiet apache2 || failures=$((failures + 1)) + diagnostic_result "Apache rewrite module is enabled" apache_rewrite_enabled || failures=$((failures + 1)) + diagnostic_result "live captive redirect matches its template" cmp -s "$CAPTIVE_TEMPLATE" "$CAPTIVE_LIVE" || failures=$((failures + 1)) + diagnostic_result "Pivilion dnsmasq is active" systemctl is-active --quiet pivilion-dnsmasq.service || failures=$((failures + 1)) + diagnostic_result "captive DNS resolves Android probe" dns_points_to_portal connectivitycheck.gstatic.com || failures=$((failures + 1)) + diagnostic_result "Pivilion nftables table is active" nft_portal_table_active || failures=$((failures + 1)) + diagnostic_result "Android probe receives portal redirect" probe_redirect connectivitycheck.gstatic.com /generate_204 || failures=$((failures + 1)) + diagnostic_result "Apple probe receives portal redirect" probe_redirect captive.apple.com /hotspot-detect.html || failures=$((failures + 1)) + diagnostic_result "Microsoft probe receives portal redirect" probe_redirect www.msftconnecttest.com /connecttest.txt || failures=$((failures + 1)) + [ "$failures" -eq 0 ] || { + echo "$failures hotspot diagnostic check(s) failed." >&2 + return 1 + } +} stop_hotspot() { systemctl stop pivilion-dnsmasq.service 2>/dev/null || true @@ -56,6 +127,7 @@ EOF start_hotspot() { stop_hotspot + install_captive_redirect rfkill unblock wifi 2>/dev/null || true systemctl start apache2 if pivilion_uses_networkmanager; then @@ -87,6 +159,7 @@ EOF dnsmasq --test --conf-file="$DNSMASQ_CONF" systemctl restart pivilion-dnsmasq.service start_firewall + hotspot_diagnostics echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE (10.1.1.1)." } @@ -120,7 +193,7 @@ start_onion() { 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 - rm -f "$PIVILION_WEBROOT/gen/.htaccess" + rm -f "$CAPTIVE_LIVE" systemctl start apache2 if systemctl cat tor@default.service >/dev/null 2>&1; then systemctl restart tor@default.service @@ -134,5 +207,6 @@ start_onion() { case "$MODE" in hotspot) start_hotspot ;; onion) start_onion ;; + diagnose) hotspot_diagnostics ;; *) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;; esac