259 lines
7.4 KiB
Bash
259 lines
7.4 KiB
Bash
#!/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 "$@"
|