#!/bin/bash # ROA2WEB SSH Tunnel Manager (Multi-Server Support) # Manages SSH tunnels to Oracle server(s) for development # # Configuration: # - Reads from backend/ssh-tunnels.json (next to .env) # - SSH passwords from backend/secrets/{server_id}.ssh_pass # - SSH keys from backend/secrets/{server_id}.ssh_key or ssh_key field in config # - Falls back to legacy single-server config from backend/.env if JSON not found # # Usage: ./ssh-tunnel.sh {start|stop|status|restart|help} set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ENV_FILE="$SCRIPT_DIR/backend/.env" SECRETS_DIR="$SCRIPT_DIR/backend/secrets" SSH_TUNNELS_FILE="$SCRIPT_DIR/backend/ssh-tunnels.json" PID_DIR="/tmp/roa_tunnels" mkdir -p "$PID_DIR" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # ============================================================================ # Parse configuration from .env and secrets/ # ============================================================================ # Global arrays to store tunnel configs declare -a SERVER_IDS declare -A SERVER_NAMES declare -A LOCAL_PORTS declare -A SSH_HOSTS declare -A SSH_PORTS declare -A SSH_USERS declare -A SSH_PASSWORDS declare -A SSH_KEYS declare -A ORACLE_HOSTS declare -A ORACLE_PORTS parse_config() { SERVER_IDS=() # Primary: Read from secrets/ssh-tunnels.json if [ -f "$SSH_TUNNELS_FILE" ]; then echo -e "${CYAN}Reading config from: $SSH_TUNNELS_FILE${NC}" # Parse JSON with Python eval "$(python3 << PYTHON_SCRIPT import json import os secrets_dir = "$SECRETS_DIR" tunnels_file = "$SSH_TUNNELS_FILE" with open(tunnels_file, 'r') as f: tunnels = json.load(f) for t in tunnels: sid = t.get('id', 'default') ssh_host = t.get('ssh_host', '') # Skip entries without ssh_host if not ssh_host: print(f'# Skipping [{sid}] - no ssh_host configured') continue print(f'SERVER_IDS+=("{sid}")') print(f'SERVER_NAMES["{sid}"]="{t.get("name", sid)}"') print(f'LOCAL_PORTS["{sid}"]="{t.get("local_port", 1521)}"') print(f'SSH_HOSTS["{sid}"]="{ssh_host}"') print(f'SSH_PORTS["{sid}"]="{t.get("ssh_port", 22)}"') print(f'SSH_USERS["{sid}"]="{t.get("ssh_user", "root")}"') print(f'ORACLE_HOSTS["{sid}"]="{t.get("oracle_host", "localhost")}"') print(f'ORACLE_PORTS["{sid}"]="{t.get("oracle_port", 1521)}"') # SSH key path (from config or default) ssh_key = t.get('ssh_key', f'{secrets_dir}/{sid}.ssh_key') # Resolve relative paths if not ssh_key.startswith('/'): ssh_key = f'$SCRIPT_DIR/backend/{ssh_key}' print(f'SSH_KEYS["{sid}"]="{ssh_key}"') PYTHON_SCRIPT )" # Load SSH passwords from secrets files for sid in "${SERVER_IDS[@]}"; do local pass_file="$SECRETS_DIR/${sid}.ssh_pass" if [ -f "$pass_file" ]; then SSH_PASSWORDS["$sid"]="$(cat "$pass_file" | tr -d '\n')" fi done fi # Fallback: Legacy single-server config from .env if [ ${#SERVER_IDS[@]} -eq 0 ]; then if [ -f "$ENV_FILE" ]; then echo -e "${YELLOW}No ssh-tunnels.json found, using legacy config from .env${NC}" # Source .env for legacy vars set -a source "$ENV_FILE" set +a fi SERVER_IDS+=("default") SERVER_NAMES["default"]="Default Server (Legacy)" LOCAL_PORTS["default"]="${ORACLE_PORT:-1521}" SSH_HOSTS["default"]="${SSH_HOST:-roa.romfast.ro}" SSH_PORTS["default"]="${SSH_PORT:-22122}" SSH_USERS["default"]="${SSH_USER:-roa2web}" ORACLE_HOSTS["default"]="${ORACLE_TUNNEL_REMOTE:-10.0.20.36}" ORACLE_PORTS["default"]="${ORACLE_TUNNEL_PORT:-1521}" SSH_KEYS["default"]="$SECRETS_DIR/roa_oracle_server" # Legacy password file if [ -f "$SECRETS_DIR/default.ssh_pass" ]; then SSH_PASSWORDS["default"]="$(cat "$SECRETS_DIR/default.ssh_pass" | tr -d '\n')" fi fi } # ============================================================================ # Tunnel management functions # ============================================================================ get_pid_file() { local sid=$1 echo "$PID_DIR/tunnel_${sid}.pid" } check_tunnel() { local sid=$1 local pid_file=$(get_pid_file "$sid") if [ -f "$pid_file" ]; then local pid=$(cat "$pid_file") if ps -p "$pid" > /dev/null 2>&1; then return 0 # Running else rm -f "$pid_file" fi fi return 1 # Not running } start_single_tunnel() { local sid=$1 local name="${SERVER_NAMES[$sid]}" local local_port="${LOCAL_PORTS[$sid]}" local ssh_host="${SSH_HOSTS[$sid]}" local ssh_port="${SSH_PORTS[$sid]}" local ssh_user="${SSH_USERS[$sid]}" local ssh_key="${SSH_KEYS[$sid]}" local ssh_pass="${SSH_PASSWORDS[$sid]}" local oracle_host="${ORACLE_HOSTS[$sid]}" local oracle_port="${ORACLE_PORTS[$sid]}" local pid_file=$(get_pid_file "$sid") echo -e "${CYAN}[$sid]${NC} ${name}" echo -e " Tunnel: localhost:${local_port} → ${oracle_host}:${oracle_port}" echo -e " Via: ${ssh_user}@${ssh_host}:${ssh_port}" if check_tunnel "$sid"; then echo -e " ${YELLOW}⚠️ Already running (PID: $(cat $pid_file))${NC}" return 0 fi # Prepare SSH key if exists local tmp_key="/tmp/ssh_key_${sid}" local use_key=false if [ -f "$ssh_key" ]; then cp "$ssh_key" "$tmp_key" chmod 600 "$tmp_key" use_key=true fi # Build SSH command local ssh_cmd="ssh -f -N -L ${local_port}:${oracle_host}:${oracle_port}" ssh_cmd+=" -p ${ssh_port}" ssh_cmd+=" -o StrictHostKeyChecking=no" ssh_cmd+=" -o ServerAliveInterval=60" ssh_cmd+=" -o ServerAliveCountMax=3" ssh_cmd+=" -o ExitOnForwardFailure=yes" if [ "$use_key" = true ]; then ssh_cmd+=" -i ${tmp_key}" ssh_cmd+=" ${ssh_user}@${ssh_host}" # Execute with key eval "$ssh_cmd" 2>/dev/null elif [ -n "$ssh_pass" ]; then # Use sshpass for password authentication if ! command -v sshpass &> /dev/null; then echo -e " ${RED}❌ sshpass not installed (needed for password auth)${NC}" echo -e " ${YELLOW} Install: sudo apt install sshpass${NC}" return 1 fi ssh_cmd+=" ${ssh_user}@${ssh_host}" sshpass -p "$ssh_pass" $ssh_cmd 2>/dev/null else echo -e " ${RED}❌ No SSH key or password found${NC}" echo -e " ${YELLOW} Add: secrets/${sid}.ssh_key or secrets/${sid}.ssh_pass${NC}" return 1 fi # Check if tunnel started sleep 1 local tunnel_pid=$(pgrep -f "ssh.*-L ${local_port}:${oracle_host}:${oracle_port}" | head -1) if [ -n "$tunnel_pid" ]; then echo "$tunnel_pid" > "$pid_file" echo -e " ${GREEN}✅ Started (PID: $tunnel_pid)${NC}" # Test connectivity if timeout 3 bash -c "cat < /dev/null > /dev/tcp/127.0.0.1/$local_port" 2>/dev/null; then echo -e " ${GREEN}✅ Port $local_port accessible${NC}" else echo -e " ${YELLOW}⚠️ Port $local_port not responding (Oracle may be down)${NC}" fi return 0 else echo -e " ${RED}❌ Failed to start tunnel${NC}" return 1 fi } stop_single_tunnel() { local sid=$1 local pid_file=$(get_pid_file "$sid") local name="${SERVER_NAMES[$sid]}" echo -e "${CYAN}[$sid]${NC} ${name}" if [ -f "$pid_file" ]; then local pid=$(cat "$pid_file") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null rm -f "$pid_file" echo -e " ${GREEN}✅ Stopped (was PID: $pid)${NC}" else rm -f "$pid_file" echo -e " ${YELLOW}⚠️ Was not running${NC}" fi else echo -e " ${YELLOW}⚠️ Was not running${NC}" fi } status_single_tunnel() { local sid=$1 local name="${SERVER_NAMES[$sid]}" local local_port="${LOCAL_PORTS[$sid]}" local pid_file=$(get_pid_file "$sid") if check_tunnel "$sid"; then local pid=$(cat "$pid_file") echo -e "${GREEN}●${NC} ${CYAN}[$sid]${NC} ${name}" echo -e " localhost:${local_port} (PID: $pid)" if timeout 2 bash -c "cat < /dev/null > /dev/tcp/127.0.0.1/$local_port" 2>/dev/null; then echo -e " ${GREEN}Port accessible${NC}" else echo -e " ${YELLOW}Port not responding${NC}" fi else echo -e "${RED}○${NC} ${CYAN}[$sid]${NC} ${name}" echo -e " localhost:${local_port} (stopped)" fi } # ============================================================================ # Main commands # ============================================================================ print_header() { echo -e "${BLUE}════════════════════════════════════════════${NC}" echo -e "${BLUE} ROA2WEB SSH Tunnel Manager (Multi-Server)${NC}" echo -e "${BLUE}════════════════════════════════════════════${NC}" echo } cmd_start() { print_header parse_config echo -e "${BLUE}Starting ${#SERVER_IDS[@]} tunnel(s)...${NC}" echo local failed=0 for sid in "${SERVER_IDS[@]}"; do start_single_tunnel "$sid" || ((failed++)) echo done if [ $failed -gt 0 ]; then echo -e "${YELLOW}⚠️ $failed tunnel(s) failed to start${NC}" return 1 else echo -e "${GREEN}✅ All tunnels started successfully${NC}" fi } cmd_stop() { print_header parse_config echo -e "${BLUE}Stopping tunnels...${NC}" echo for sid in "${SERVER_IDS[@]}"; do stop_single_tunnel "$sid" done echo echo -e "${GREEN}✅ All tunnels stopped${NC}" } cmd_status() { print_header parse_config echo -e "${BLUE}Tunnel Status:${NC}" echo "────────────────────────────────────────────" for sid in "${SERVER_IDS[@]}"; do status_single_tunnel "$sid" echo done } cmd_restart() { cmd_stop sleep 2 cmd_start } cmd_help() { print_header parse_config echo -e "${BLUE}Usage:${NC} $0 {start|stop|status|restart|help}" echo echo -e "${BLUE}Commands:${NC}" echo " start - Start all configured SSH tunnels" echo " stop - Stop all SSH tunnels" echo " status - Show status of all tunnels" echo " restart - Restart all tunnels" echo " help - Show this help" echo echo -e "${BLUE}Configuration:${NC}" echo " SSH Tunnels: $SSH_TUNNELS_FILE" echo " Secrets: $SECRETS_DIR/" echo " Fallback: $ENV_FILE (legacy mode)" echo echo -e "${BLUE}Configured Tunnels (${#SERVER_IDS[@]}):${NC}" for sid in "${SERVER_IDS[@]}"; do echo " - [$sid] ${SERVER_NAMES[$sid]}" echo " localhost:${LOCAL_PORTS[$sid]} → ${ORACLE_HOSTS[$sid]}:${ORACLE_PORTS[$sid]}" echo " via ${SSH_USERS[$sid]}@${SSH_HOSTS[$sid]}:${SSH_PORTS[$sid]}" done echo echo -e "${BLUE}Secrets Files (per server_id):${NC}" echo " secrets/{id}.ssh_key - SSH private key (preferred)" echo " secrets/{id}.ssh_pass - SSH password (fallback, needs sshpass)" echo echo -e "${BLUE}Example ssh-tunnels.json:${NC}" echo ' [{"id": "romfast", "name": "Romfast", "local_port": 1521,' echo ' "ssh_host": "roa.romfast.ro", "ssh_port": 22122, "ssh_user": "roa2web",' echo ' "oracle_host": "10.0.20.36", "oracle_port": 1521}]' echo } # ============================================================================ # Main # ============================================================================ case "${1:-help}" in start) cmd_start ;; stop) cmd_stop ;; status) cmd_status ;; restart) cmd_restart ;; help|--help|-h) cmd_help ;; *) echo -e "${RED}❌ Unknown command: $1${NC}" echo cmd_help exit 1 ;; esac