diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e71376c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +AGENTS.md +CLAUDE.md +*.qcow2 +*.img +*.sha1sum +*.xz +*.zst diff --git a/pi4/build/build-stormux.sh b/pi4/build/build-stormux.sh index ba4dc0b..71c96cf 100755 --- a/pi4/build/build-stormux.sh +++ b/pi4/build/build-stormux.sh @@ -25,16 +25,24 @@ set -e # Don't want to destroy stuff if this goes majorly wrong. trap cleanup EXIT # make sure the script cleans up after itself before closing. +# shellcheck disable=SC2329 # cleanup is invoked via trap EXIT cleanup() { + status=$? # capture original exit status so failures propagate + if [[ $mounted -eq 0 ]]; then - umount -R /mnt - partx -d "${loopdev}" - losetup --detach "${loopdev}" + umount -R /mnt || true fi - if [[ -n "${imageFileName}" ]]; then + + if [[ -n "${loopdev:-}" ]]; then + partx -d "${loopdev}" || true + losetup --detach "${loopdev}" || true + fi + + if [[ -n "${imageFileName:-}" ]]; then rm "${imageFileName}" fi - exit 0 + + exit "$status" } help() { @@ -122,7 +130,7 @@ imageUrl="http://os.archlinuxarm.org/os/ArchLinuxARM-rpi-aarch64-latest.tar.gz" fallocate -l "$imageSize" "$imageName" loopdev="$(losetup --find --show "${imageName}")" -parted --script "${loopdev}" mklabel msdos mkpart primary fat32 0% 200M mkpart primary ext4 200M 100% +parted --script "${loopdev}" mklabel msdos mkpart primary fat32 0% 512M mkpart primary ext4 512M 100% mkfs.vfat -F32 -n STRMX_BOOT "${loopdev}p1" mkfs.ext4 -F -L STRMX_ROOT "${loopdev}p2" mount "${loopdev}p2" /mnt @@ -130,7 +138,7 @@ mkdir /mnt/boot mount "${loopdev}p1" /mnt/boot # Things are mounted now, so set mounted to 0 (bash true) mounted=0 -imageFileName=$(mktemp) +imageFileName=$(mktemp -p . ArchLinuxARM-rpi-aarch64-XXXXXX.tar.gz) wget "${imageUrl}" -O "${imageFileName}" bsdtar -xpf "${imageFileName}" -C /mnt @@ -143,6 +151,7 @@ find ../files/etc -mindepth 1 -maxdepth 1 ! -name skel -exec cp -rv {} /mnt/etc/ PS1="(Chroot) [\u@\h \W] \$" arch-chroot /mnt << EOF echo "Chroot started." +set -euo pipefail # set up pacman pacman-key --init pacman-key --populate archlinuxarm @@ -301,6 +310,7 @@ services=( log-to-ram-sync.timer log-to-ram-shutdown.service NetworkManager.service + ssh-login-monitor.service ) for service in "\${services[@]}"; do @@ -311,8 +321,7 @@ for service in "\${services[@]}"; do echo " \$service: FAILED" fi done -# Cleanup packages -pacman -Runcds libx11 --noconfirm + pacman -Sc --noconfirm EOF @@ -320,5 +329,8 @@ EOF # Copy skel files to stormux user home (after user rename in chroot) find ../files/etc/skel/ -mindepth 1 -exec cp -rv "{}" /mnt/home/stormux/ \; +# Copy boot files again to ensure custom config overrides any package changes +cp -rv ../files/boot/* /mnt/boot + # Exiting calls the cleanup function to unmount. exit 0 diff --git a/pi4/files/etc/systemd/system/ssh-login-monitor.service b/pi4/files/etc/systemd/system/ssh-login-monitor.service new file mode 100644 index 0000000..497c948 --- /dev/null +++ b/pi4/files/etc/systemd/system/ssh-login-monitor.service @@ -0,0 +1,17 @@ +[Unit] +Description=Fenrir SSH Login Monitor +After=sshd.service +Wants=sshd.service + +[Service] +Type=simple +ExecStart=/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh +Restart=on-failure +RestartSec=5 + +# Security settings +NoNewPrivileges=true +PrivateTmp=false + +[Install] +WantedBy=multi-user.target diff --git a/pi4/files/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh b/pi4/files/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh new file mode 100755 index 0000000..494d0e0 --- /dev/null +++ b/pi4/files/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# SSH Login Monitor for Fenrir Screen Reader +# Monitors SSH logins and announces them via Fenrir's speech system + +# Configuration +fenrirSocket="/tmp/fenrirscreenreader-deamon.sock" +logFile="/var/log/auth.log" +stateFile="/tmp/fenrir-ssh-monitor.state" +checkInterval=2 # seconds between checks + +# Voice settings +announceUser=true +announceIp=true +announceHostname=true +announceLogout=false # Announce SSH disconnections (disabled by default - may not work reliably on all systems) + +# Function to send message to Fenrir +fenrirSay() { + local message="$1" + # Only announce if Fenrir socket exists (silently skip if not) + if [[ -S "$fenrirSocket" ]]; then + echo "command say ${message}" | socat - UNIX-CLIENT:"${fenrirSocket}" 2>/dev/null + fi +} + +# Function to get last processed line number +getLastLine() { + if [[ -f "$stateFile" ]]; then + cat "$stateFile" + else + echo "0" + fi +} + +# Function to save last processed line number +saveLastLine() { + echo "$1" > "$stateFile" +} + +# Function to parse SSH login and announce +processLogin() { + local logLine="$1" + local user="" + local ip="" + local hostname="" + + # Parse different SSH login patterns + # Pattern 1: "Accepted publickey for USER from IP" + # Pattern 2: "Accepted password for USER from IP" + if [[ "$logLine" =~ Accepted\ (publickey|password|keyboard-interactive/pam)\ for\ ([^[:space:]]+)\ from\ ([^[:space:]]+) ]]; then + user="${BASH_REMATCH[2]}" + ip="${BASH_REMATCH[3]}" + + # Try to resolve hostname + if command -v host &> /dev/null && [[ "$announceHostname" == "true" ]]; then + hostname="$(host "$ip" 2>/dev/null | grep -oP 'domain name pointer \K[^.]+' | head -1)" + fi + + # Build announcement message (concise format) + local message="" + + if [[ "$announceUser" == "true" ]]; then + message+="${user} " + fi + + message+="S S H login" + + if [[ "$announceIp" == "true" ]]; then + message+=" from ${ip}" + fi + + if [[ -n "$hostname" ]] && [[ "$announceHostname" == "true" ]]; then + message+=" ${hostname}" + fi + + fenrirSay "$message" + return 0 + fi + + return 1 +} + +# Function to parse SSH logout and announce +processLogout() { + local logLine="$1" + local user="" + + # Parse SSH disconnect patterns + # Pattern: "pam_unix(sshd:session): session closed for user USER" + if [[ "$logLine" =~ session\ closed\ for\ user\ ([^[:space:]]+) ]]; then + user="${BASH_REMATCH[1]}" + + if [[ "$announceLogout" == "true" ]]; then + local message="" + + if [[ "$announceUser" == "true" ]]; then + message+="${user} " + fi + + message+="disconnected from S S H" + + fenrirSay "$message" + fi + return 0 + fi + + return 1 +} + +# Function to monitor auth.log +monitorAuthLog() { + local lastLine + lastLine=$(getLastLine) + + # Get total lines in log + local totalLines + totalLines=$(wc -l < "$logFile" 2>/dev/null || echo "0") + + # If log was rotated, reset + if [[ $totalLines -lt $lastLine ]]; then + lastLine=0 + fi + + # Process new lines + if [[ $totalLines -gt $lastLine ]]; then + local newLines=$((totalLines - lastLine)) + + # Read only new lines + tail -n "$newLines" "$logFile" 2>/dev/null | while IFS= read -r line; do + if [[ "$line" =~ sshd.*Accepted ]]; then + processLogin "$line" + elif [[ "$line" =~ sshd.*session\ closed ]]; then + processLogout "$line" + fi + done + + saveLastLine "$totalLines" + fi +} + +# Function to monitor journalctl (alternative for systemd systems) +monitorJournalctl() { + # Follow journalctl for SSH logins and logouts + journalctl -u sshd -u ssh -f -n 0 --no-pager 2>/dev/null | while IFS= read -r line; do + if [[ "$line" =~ Accepted ]]; then + processLogin "$line" + elif [[ "$line" =~ session\ closed ]]; then + processLogout "$line" + fi + done +} + +# Check if running as root +if [[ "$(id -u)" -ne 0 ]]; then + echo "Error: This script must be run with sudo privileges to access system logs." + exit 1 +fi + +# Note: We don't require Fenrir to be running at startup +# The script will silently skip announcements when Fenrir socket doesn't exist + +# Determine monitoring method +if command -v journalctl &> /dev/null && systemctl is-active --quiet sshd 2>/dev/null; then + echo "Starting SSH login monitor (using journalctl)..." + fenrirSay "SSH login monitor started." + + # Use journalctl for real-time monitoring + trap 'fenrirSay "SSH login monitor stopped."; exit 0' INT TERM + monitorJournalctl +elif [[ -f "$logFile" ]]; then + echo "Starting SSH login monitor (using auth.log)..." + fenrirSay "SSH login monitor started." + + # Use auth.log polling + trap 'fenrirSay "SSH login monitor stopped."; rm -f "$stateFile"; exit 0' INT TERM + + while true; do + monitorAuthLog + sleep "$checkInterval" + done +else + echo "Error: Cannot find SSH logs. Neither journalctl nor ${logFile} is available." + exit 1 +fi