#!/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 "$@"