diff --git a/x86_64/airootfs/etc/stormux-assist/client.conf b/x86_64/airootfs/etc/stormux-assist/client.conf new file mode 100644 index 0000000..3b32405 --- /dev/null +++ b/x86_64/airootfs/etc/stormux-assist/client.conf @@ -0,0 +1,15 @@ +# Stormux Assistance System - Client Configuration + +[server] +host = assistance.stormux.org +ssh_port = 22 +ssh_user = stormux-assist +tunnel_port = 2222 + +[session] +# Session timeout in seconds (14400 = 4 hours) +timeout = 14400 + +[client] +log_file = /var/log/sas.log +log_dir = ~/stormux-assist-logs/ diff --git a/x86_64/airootfs/etc/systemd/system/multi-user.target.wants/ssh-login-monitor.service b/x86_64/airootfs/etc/systemd/system/multi-user.target.wants/ssh-login-monitor.service new file mode 120000 index 0000000..dc5d27f --- /dev/null +++ b/x86_64/airootfs/etc/systemd/system/multi-user.target.wants/ssh-login-monitor.service @@ -0,0 +1 @@ +../ssh-login-monitor.service \ No newline at end of file diff --git a/x86_64/airootfs/etc/systemd/system/ssh-login-monitor.service b/x86_64/airootfs/etc/systemd/system/ssh-login-monitor.service new file mode 100644 index 0000000..497c948 --- /dev/null +++ b/x86_64/airootfs/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/x86_64/airootfs/usr/local/bin/install-stormux b/x86_64/airootfs/usr/local/bin/install-stormux index 2368492..4003fdf 100755 --- a/x86_64/airootfs/usr/local/bin/install-stormux +++ b/x86_64/airootfs/usr/local/bin/install-stormux @@ -850,7 +850,7 @@ install_base_system() { # Add desktop-specific packages case "$desktopEnvironment" in i3) - allPackages+=(i3-wm orca lxterminal pluma) + allPackages+=(i3-wm orca python-psutil lxterminal pluma) allPackages+=(discount jq libnotify xfce4-notifyd pamixer playerctl) allPackages+=(python-i3ipc python-wxpython sox yad) allPackages+=(lxsession magic-wormhole pcmanfm) @@ -860,7 +860,7 @@ install_base_system() { stormuxPackages+=(xlibre-xserver xlibre-input-libinput nodm-dgw brave-bin) ;; mate) - allPackages+=(mate mate-extra orca) + allPackages+=(mate mate-extra orca python-psutil) # Add Stormux-specific MATE packages stormuxPackages+=(xlibre-xserver xlibre-input-libinput nodm-dgw brave-bin) ;; @@ -1017,6 +1017,37 @@ EOF log_info "Audio configuration installed" } +# +# SSH login monitor installation (script + service) +# + +install_ssh_login_monitor() { + log_info "=== Installing SSH Login Monitor ===" + + local srcScript="/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh" + local srcService="/etc/systemd/system/ssh-login-monitor.service" + + if [[ ! -f "$srcScript" ]]; then + log_error "Source script not found: $srcScript" + return 1 + fi + if [[ ! -f "$srcService" ]]; then + log_error "Source service not found: $srcService" + return 1 + fi + + mkdir -p "$mountPoint/usr/share/fenrirscreenreader/scripts" + mkdir -p "$mountPoint/etc/systemd/system" + + cp "$srcScript" "$mountPoint/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh" + chmod 755 "$mountPoint/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh" + + cp "$srcService" "$mountPoint/etc/systemd/system/ssh-login-monitor.service" + chmod 644 "$mountPoint/etc/systemd/system/ssh-login-monitor.service" + + log_info "SSH login monitor installed to target system" +} + # # System configuration in chroot # @@ -1156,6 +1187,7 @@ systemctl enable NetworkManager.service systemctl enable brltty.path systemctl enable fenrirscreenreader.service systemctl enable cronie.service +systemctl enable ssh-login-monitor.service # Enable bluetooth if present systemctl enable bluetooth.service 2>/dev/null || true @@ -1588,6 +1620,13 @@ if ! install_audio_configs; then exit 1 fi +# Install SSH login monitor files (script + service) +if ! install_ssh_login_monitor; then + log_error "SSH login monitor installation failed" + cleanup_and_unmount + exit 1 +fi + # Configure system if ! configure_system; then log_error "System configuration failed" diff --git a/x86_64/airootfs/usr/local/bin/sas b/x86_64/airootfs/usr/local/bin/sas new file mode 120000 index 0000000..741a126 --- /dev/null +++ b/x86_64/airootfs/usr/local/bin/sas @@ -0,0 +1 @@ +sas.sh \ No newline at end of file diff --git a/x86_64/airootfs/usr/local/bin/sas.sh b/x86_64/airootfs/usr/local/bin/sas.sh new file mode 100644 index 0000000..46357ef --- /dev/null +++ b/x86_64/airootfs/usr/local/bin/sas.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# Stormux Assistance System (SAS) - Client +# Simple command for users to request remote assistance +# Usage: sas + +set -euo pipefail + +# Configuration +serverHost="assistance.stormux.org" +serverPort=22 +serverUser="stormux-assist" +tunnelPort=2222 +configFile="/etc/stormux-assist/client.conf" +logFile="/var/log/sas.log" +logDir="${HOME}/stormux-assist-logs" +sessionTimeout=14400 # 4 hours + +# Session variables +sessionId="" +tunnelPid="" + +# Speech feedback function +speak() { + spd-say -w "$1" 2>/dev/null || true +} + +# Logging function (format: "Message [timestamp]") +logMessage() { + local message="$1" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "${message} [${timestamp}]" >> "${logFile}" +} + +# Error handler with speech +errorExit() { + local message="$1" + speak "Error: ${message}" + logMessage "ERROR: ${message}" + cleanup + exit 1 +} + +# Load configuration if it exists +loadConfig() { + if [[ -f "${configFile}" ]]; then + # Source config file values (simple INI parsing) + while IFS='=' read -r key value; do + # Skip comments and empty lines + [[ "${key}" =~ ^[[:space:]]*# ]] && continue + [[ -z "${key}" ]] && continue + + # Trim whitespace + key=$(echo "${key}" | xargs) + value=$(echo "${value}" | xargs) + + case "${key}" in + host) serverHost="${value}" ;; + ssh_port) serverPort="${value}" ;; + ssh_user) serverUser="${value}" ;; + tunnel_port) tunnelPort="${value}" ;; + timeout) sessionTimeout="${value}" ;; + log_file) logFile="${value}" ;; + log_dir) logDir="${value}" ;; + esac + done < "${configFile}" + logMessage "Configuration loaded from ${configFile}" + else + logMessage "No config file found, using defaults" + fi +} + +# Check network connectivity +checkNetwork() { + speak "Checking network connection" + logMessage "Checking network connectivity to ${serverHost}" + + if ! ping -c 1 -W 5 "${serverHost}" &>/dev/null; then + errorExit "No network connection detected. Please connect to the internet and try again." + fi + + logMessage "Network connectivity verified" +} + +# Check SSH client is installed +checkSsh() { + if ! command -v ssh &>/dev/null; then + errorExit "SSH client not found. Please install openssh." + fi + + if ! command -v autossh &>/dev/null; then + errorExit "autossh not found. Please install autossh package." + fi + + logMessage "SSH and autossh verified" +} + +# Create log directory +createLogDir() { + if [[ ! -d "${logDir}" ]]; then + mkdir -p "${logDir}" || errorExit "Failed to create log directory ${logDir}" + logMessage "Created log directory ${logDir}" + fi +} + +# Generate session ID +generateSessionId() { + sessionId=$(date '+%Y%m%d-%H%M%S')-$(hostname -s) + logMessage "Generated session ID: ${sessionId}" +} + +# Establish SSH reverse tunnel +establishTunnel() { + speak "Establishing connection to assistance server" + logMessage "Establishing SSH reverse tunnel to ${serverHost}:${serverPort}" + + # Use autossh for auto-reconnection + # -M 0: disable autossh monitoring port (use ServerAliveInterval instead) + # -N: no remote command + # -R 2222:localhost:22: reverse tunnel from server port 2222 to local port 22 + # ServerAliveInterval: keep connection alive + # ServerAliveCountMax: max failed keepalives before disconnect + # ExitOnForwardFailure: exit if tunnel cannot be established + + autossh -M 0 \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "StrictHostKeyChecking=accept-new" \ + -N \ + -R "${tunnelPort}:localhost:22" \ + -p "${serverPort}" \ + "${serverUser}@${serverHost}" & + + tunnelPid=$! + + # Wait a moment for tunnel to establish + sleep 3 + + # Check if tunnel is still running + if ! kill -0 "${tunnelPid}" 2>/dev/null; then + errorExit "Failed to establish SSH tunnel. Please check your SSH keys and server accessibility." + fi + + logMessage "SSH tunnel established (PID: ${tunnelPid})" +} + +# Wait for user to quit or timeout +waitForQuit() { + speak "Connection established. Support staff have been notified via IRC. Press Q to quit or wait for support to connect." + logMessage "Session active, waiting for Q keypress or timeout" + + echo "" + echo "════════════════════════════════════════════════" + echo " Stormux Assistance System - Session Active" + echo "════════════════════════════════════════════════" + echo "" + echo "Support staff have been notified via IRC." + echo "They will connect shortly to help you." + echo "" + echo "Session ID: ${sessionId}" + echo "Session timeout: 4 hours" + echo "" + echo "Press 'Q' to end the session early" + echo "" + echo "════════════════════════════════════════════════" + echo "" + + local startTime + startTime=$(date +%s) + + while true; do + # Check for timeout + local currentTime + currentTime=$(date +%s) + local elapsed=$((currentTime - startTime)) + + if [[ ${elapsed} -ge ${sessionTimeout} ]]; then + speak "Session has timed out after 4 hours" + logMessage "Session timed out after ${sessionTimeout} seconds" + break + fi + + # Check for Q keypress (with timeout) + if read -r -t 1 -n 1 key; then + if [[ "${key}" == "q" ]] || [[ "${key}" == "Q" ]]; then + speak "Ending assistance session" + logMessage "User requested session termination" + break + fi + fi + + # Check if tunnel is still alive + if ! kill -0 "${tunnelPid}" 2>/dev/null; then + speak "Connection lost. Session ended." + logMessage "Tunnel process died unexpectedly" + break + fi + done +} + +# Cleanup and exit +cleanup() { + logMessage "Starting cleanup" + + # Kill tunnel if running + if [[ -n "${tunnelPid}" ]] && kill -0 "${tunnelPid}" 2>/dev/null; then + kill "${tunnelPid}" 2>/dev/null || true + logMessage "Tunnel process terminated" + fi + + logMessage "Cleanup complete" +} + +# End-of-session patronage message +patronageMessage() { + speak "Assistance session ended. Thank you for using Stormux Live Assistance." + sleep 2 + speak "This service is made possible by supporters like you. Consider becoming a patron at patreon dot com slash stormux to help keep this service running." + + logMessage "Session ended, patronage message delivered" +} + +# Main execution +main() { + speak "Starting Stormux assistance request" + logMessage "=== SAS Client Started ===" + + # Ensure we clean up on exit + trap cleanup EXIT INT TERM + + # Load configuration + loadConfig + + # Create log directory + createLogDir + + # Pre-flight checks + checkNetwork + checkSsh + + # Generate session ID + generateSessionId + + # Establish tunnel + establishTunnel + + # Wait for quit or timeout + waitForQuit + + # End session + patronageMessage + + logMessage "=== SAS Client Ended ===" +} + +# Run main +main "$@" diff --git a/x86_64/airootfs/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh b/x86_64/airootfs/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh new file mode 100755 index 0000000..494d0e0 --- /dev/null +++ b/x86_64/airootfs/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 diff --git a/x86_64/packages.x86_64 b/x86_64/packages.x86_64 index 2fb9313..f689f46 100644 --- a/x86_64/packages.x86_64 +++ b/x86_64/packages.x86_64 @@ -3,6 +3,7 @@ alsa-utils amd-ucode arch-install-scripts archinstall +autossh base base-devel bash-completion diff --git a/x86_64/profiledef.sh b/x86_64/profiledef.sh index 510deaf..9453a2a 100644 --- a/x86_64/profiledef.sh +++ b/x86_64/profiledef.sh @@ -29,6 +29,10 @@ file_permissions=( ["/usr/local/bin/install-stormux"]="0:0:755" ["/usr/local/bin/stormux-setup.sh"]="0:0:755" ["/usr/local/bin/init-pipewire-sound.sh"]="0:0:755" + ["/usr/local/bin/sas.sh"]="0:0:755" + ["/usr/local/bin/sas"]="0:0:755" + ["/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh"]="0:0:755" + ["/etc/stormux-assist"]="0:0:755" + ["/etc/stormux-assist/client.conf"]="0:0:644" ["/home/stormux"]="1000:1000:755" ) -