mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
7972 lines
312 KiB
Bash
7972 lines
312 KiB
Bash
#!/bin/bash
|
||
#===============================================================================
|
||
#
|
||
# BetterDesk Console Manager v3.5.92
|
||
# All-in-One Interactive Tool for Linux
|
||
#
|
||
# Features:
|
||
# - Fresh installation (Node.js web console)
|
||
# - Minimal installation (Go server only, no web console)
|
||
# - Update existing installation
|
||
# - Repair/fix issues (enhanced with graceful shutdown)
|
||
# - Validate installation
|
||
# - Backup & restore
|
||
# - Reset admin password
|
||
# - Build & deploy server (rebuild Go binary with rollback)
|
||
# - Full diagnostics
|
||
# - SHA256 binary verification
|
||
# - Auto mode (non-interactive)
|
||
# - Enhanced service management with health verification
|
||
# - Port conflict detection
|
||
# - Fixed ban system (device-specific, not IP-based)
|
||
# - RustDesk Client API (login, address book sync)
|
||
# - TOTP Two-Factor Authentication
|
||
# - SSL/TLS certificate configuration
|
||
# - PostgreSQL database support
|
||
# - SQLite to PostgreSQL migration
|
||
# - CDAP (Custom Device API Protocol) support
|
||
#
|
||
# Usage:
|
||
# Interactive: sudo ./betterdesk.sh
|
||
# Auto mode: sudo ./betterdesk.sh --auto
|
||
# PostgreSQL: sudo ./betterdesk.sh --auto --postgresql
|
||
#
|
||
#===============================================================================
|
||
|
||
set -e
|
||
|
||
# Version
|
||
VERSION="3.5.92"
|
||
# Bump when installer control-flow changes must apply mid-session after Update (#219).
|
||
BETTERDESK_SH_REVISION="20260725-console-start-306"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# Preserve argv before shift — used to re-exec after installer self-update (#219).
|
||
BETTERDESK_ORIG_ARGV=("$@")
|
||
|
||
# Auto mode flag
|
||
AUTO_MODE=false
|
||
SKIP_VERIFY=false
|
||
MINIMAL_MODE=false
|
||
UNINSTALL_MODE=false
|
||
PURGE_MODE=false
|
||
PREFERRED_CONSOLE_TYPE="nodejs" # Always Node.js (Flask removed in v2.3.0)
|
||
|
||
# Relay server selection mode:
|
||
# auto - detect public IP (default, best for internet-facing servers)
|
||
# local - use the server's LAN IP (best for LAN-only deployments)
|
||
# public - force public IP detection
|
||
# RELAY_SERVERS env var (or --relay-servers) always overrides this with a fixed value.
|
||
RELAY_MODE="${RELAY_MODE:-auto}"
|
||
|
||
# Parse command line arguments
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--auto|-a)
|
||
AUTO_MODE=true
|
||
shift
|
||
;;
|
||
--skip-verify)
|
||
SKIP_VERIFY=true
|
||
shift
|
||
;;
|
||
--minimal)
|
||
MINIMAL_MODE=true
|
||
shift
|
||
;;
|
||
--uninstall)
|
||
UNINSTALL_MODE=true
|
||
shift
|
||
;;
|
||
--purge)
|
||
PURGE_MODE=true
|
||
shift
|
||
;;
|
||
--nodejs)
|
||
PREFERRED_CONSOLE_TYPE="nodejs"
|
||
shift
|
||
;;
|
||
--postgresql|--postgres)
|
||
USE_POSTGRESQL=true
|
||
shift
|
||
;;
|
||
--relay-mode)
|
||
RELAY_MODE="$2"
|
||
if [ "$RELAY_MODE" != "auto" ] && [ "$RELAY_MODE" != "local" ] && [ "$RELAY_MODE" != "lan" ] && [ "$RELAY_MODE" != "public" ] && [ "$RELAY_MODE" != "wan" ]; then
|
||
echo "ERROR: --relay-mode must be 'auto', 'local' (lan) or 'public' (wan)"
|
||
exit 1
|
||
fi
|
||
shift 2
|
||
;;
|
||
--relay-servers|--relay)
|
||
RELAY_SERVERS="$2"
|
||
shift 2
|
||
;;
|
||
--protocol)
|
||
PROTOCOL_MODE="$2"
|
||
if [ "$PROTOCOL_MODE" != "http" ] && [ "$PROTOCOL_MODE" != "https" ]; then
|
||
echo "ERROR: --protocol must be 'http' or 'https'"
|
||
exit 1
|
||
fi
|
||
shift 2
|
||
;;
|
||
--pg-uri)
|
||
POSTGRESQL_URI="$2"
|
||
USE_POSTGRESQL=true
|
||
shift 2
|
||
;;
|
||
--flask)
|
||
echo "WARNING: Flask console is deprecated and no longer available in v2.3.0"
|
||
echo "Node.js console will be installed instead."
|
||
PREFERRED_CONSOLE_TYPE="nodejs"
|
||
shift
|
||
;;
|
||
--help|-h)
|
||
echo "BetterDesk Console Manager v$VERSION"
|
||
echo ""
|
||
echo "Usage: sudo ./betterdesk.sh [OPTIONS]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --auto, -a Run in automatic mode (non-interactive)"
|
||
echo " --uninstall Stop services and remove the native installation"
|
||
echo " --purge With --uninstall, also remove data and keys"
|
||
echo " --skip-verify Skip SHA256 verification of binaries"
|
||
echo " --minimal Install Go server only (no web console)"
|
||
echo " --nodejs Install Node.js web console (default)"
|
||
echo " --postgresql Use PostgreSQL instead of SQLite"
|
||
echo " --pg-uri URI PostgreSQL connection URI (implies --postgresql)"
|
||
echo " --protocol MODE Set protocol mode: 'http' or 'https'"
|
||
echo " --relay-mode M Relay IP selection: 'auto' (public, default), 'local' (LAN), 'public'"
|
||
echo " --relay-servers IP Force a fixed relay server address (IP or host[:port])"
|
||
echo " --help, -h Show this help message"
|
||
echo ""
|
||
echo "Environment variables:"
|
||
echo " USE_POSTGRESQL=true Use PostgreSQL"
|
||
echo " POSTGRESQL_URI=... PostgreSQL connection URI"
|
||
echo " POSTGRESQL_USER=... PostgreSQL username (default: betterdesk)"
|
||
echo " POSTGRESQL_PASS=... PostgreSQL password (auto-generated if empty)"
|
||
echo " POSTGRESQL_DB=... PostgreSQL database (default: betterdesk)"
|
||
echo " POSTGRESQL_HOST=... PostgreSQL host (default: localhost)"
|
||
echo " POSTGRESQL_PORT=... PostgreSQL port (default: 5432)"
|
||
echo " RELAY_MODE=auto|local|public Relay IP selection mode (default: auto)"
|
||
echo " RELAY_SERVERS=... Force a fixed relay server address (overrides RELAY_MODE)"
|
||
echo " STORE_ADMIN_CREDENTIALS=true Persist admin password to .admin_credentials (not recommended)"
|
||
echo " ADMIN_PASSWORD=... Set custom admin password (default: auto-generated)"
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Go server source directory
|
||
GO_SERVER_SOURCE="$SCRIPT_DIR/betterdesk-server"
|
||
|
||
# Minimum Go version required for compilation (must match betterdesk-server/go.mod).
|
||
GO_MIN_VERSION="1.26.6"
|
||
# Point release downloaded when system Go is missing/outdated (must exist on go.dev/dl).
|
||
GO_DOWNLOAD_VERSION="1.26.6"
|
||
# Maximum time allowed for the first module download on a native install.
|
||
GO_MODULE_DOWNLOAD_TIMEOUT="${GO_MODULE_DOWNLOAD_TIMEOUT:-600}"
|
||
# Short HTTPS probe before go mod download (fail fast on blocked proxy/DNS).
|
||
GO_MODULE_PREFLIGHT_TIMEOUT="${GO_MODULE_PREFLIGHT_TIMEOUT:-20}"
|
||
# Set when preflight detects IPv4 OK but IPv6 broken (common on GCP VMs).
|
||
GO_FORCE_IPV4=0
|
||
# Toolchain for native compile (override with BETTERDESK_GOTOOLCHAIN; default local).
|
||
BETTERDESK_GOTOOLCHAIN="${BETTERDESK_GOTOOLCHAIN:-local}"
|
||
|
||
# Default paths (can be overridden by environment variables)
|
||
RUSTDESK_PATH="${RUSTDESK_PATH:-}"
|
||
CONSOLE_PATH="${CONSOLE_PATH:-}"
|
||
CONSOLE_TYPE="none" # none, nodejs
|
||
BACKUP_DIR="${BACKUP_DIR:-/opt/rustdesk-backups}"
|
||
|
||
# API (v3): handlers on Go (GO_API_PORT 21114, default/direct). :21121 is Node
|
||
# reverse-proxy for backward compatibility only. Panel is management UI (:5000).
|
||
GO_API_PORT="${GO_API_PORT:-21114}"
|
||
CLIENT_API_PORT="${CLIENT_API_PORT:-21121}"
|
||
API_PORT="${API_PORT:-$GO_API_PORT}"
|
||
STORE_ADMIN_CREDENTIALS="${STORE_ADMIN_CREDENTIALS:-false}"
|
||
|
||
# Database configuration
|
||
USE_POSTGRESQL="${USE_POSTGRESQL:-false}" # true = PostgreSQL, false = SQLite
|
||
POSTGRESQL_URI="${POSTGRESQL_URI:-}" # postgres://user:pass@host:5432/dbname
|
||
POSTGRESQL_USER="${POSTGRESQL_USER:-betterdesk}"
|
||
POSTGRESQL_PASS="${POSTGRESQL_PASS:-}"
|
||
POSTGRESQL_DB="${POSTGRESQL_DB:-betterdesk}"
|
||
POSTGRESQL_HOST="${POSTGRESQL_HOST:-localhost}"
|
||
POSTGRESQL_PORT="${POSTGRESQL_PORT:-5432}"
|
||
|
||
# Common installation paths to search
|
||
COMMON_RUSTDESK_PATHS=(
|
||
"/opt/betterdesk"
|
||
"/opt/rustdesk"
|
||
"/usr/local/rustdesk"
|
||
"/var/lib/rustdesk"
|
||
"/home/rustdesk"
|
||
"$HOME/rustdesk"
|
||
)
|
||
|
||
COMMON_CONSOLE_PATHS=(
|
||
"/opt/BetterDeskConsole"
|
||
"/opt/betterdesk"
|
||
"/var/lib/betterdesk"
|
||
"$HOME/BetterDeskConsole"
|
||
)
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
MAGENTA='\033[0;35m'
|
||
WHITE='\033[1;37m'
|
||
NC='\033[0m' # No Color
|
||
BOLD='\033[1m'
|
||
DIM='\033[2m'
|
||
|
||
# Logging
|
||
LOG_FILE="/tmp/betterdesk_$(date +%Y%m%d_%H%M%S).log"
|
||
|
||
#===============================================================================
|
||
# Helper Functions
|
||
#===============================================================================
|
||
|
||
log() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
||
}
|
||
|
||
print_header() {
|
||
clear
|
||
echo -e "${CYAN}"
|
||
echo "╔══════════════════════════════════════════════════════════════════╗"
|
||
echo "║ ║"
|
||
echo "║ ██████╗ ███████╗████████╗████████╗███████╗██████╗ ║"
|
||
echo "║ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ ║"
|
||
echo "║ ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ ║"
|
||
echo "║ ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ ║"
|
||
echo "║ ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ ║"
|
||
echo "║ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║"
|
||
echo "║ ██████╗ ███████╗███████╗██╗ ██╗ ║"
|
||
echo "║ ██╔══██╗██╔════╝██╔════╝██║ ██╔╝ ║"
|
||
echo "║ ██║ ██║█████╗ ███████╗█████╔╝ ║"
|
||
echo "║ ██║ ██║██╔══╝ ╚════██║██╔═██╗ ║"
|
||
echo "║ ██████╔╝███████╗███████║██║ ██╗ ║"
|
||
echo "║ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ║"
|
||
echo "║ ║"
|
||
echo "║ Console Manager v${VERSION} ║"
|
||
echo "╚══════════════════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}"
|
||
}
|
||
|
||
print_success() { echo -e "${GREEN}✓${NC} $1"; log "SUCCESS: $1"; }
|
||
print_error() { echo -e "${RED}✗${NC} $1"; log "ERROR: $1"; }
|
||
print_warning() { echo -e "${YELLOW}!${NC} $1"; log "WARNING: $1"; }
|
||
print_info() { echo -e "${BLUE}ℹ${NC} $1"; log "INFO: $1"; }
|
||
print_step() { echo -e "${MAGENTA}▶${NC} $1"; log "STEP: $1"; }
|
||
|
||
press_enter() {
|
||
echo ""
|
||
echo -e "${CYAN}Press Enter to continue...${NC}"
|
||
read -r
|
||
}
|
||
|
||
confirm() {
|
||
local prompt="${1:-Continue?}"
|
||
echo -e "${YELLOW}${prompt} [y/N]${NC} "
|
||
read -r response
|
||
[[ "$response" =~ ^[TtYy]$ ]]
|
||
}
|
||
|
||
#===============================================================================
|
||
# Interactive TUI (arrow-key navigable menu) — pure bash, no dependencies
|
||
#===============================================================================
|
||
# Result of tui_select() is returned in the global TUI_RESULT.
|
||
# Returns 0 on selection, 1 when TUI is unavailable (caller falls back to text).
|
||
TUI_RESULT=""
|
||
|
||
# Detect whether the modern arrow-key interface can be used.
|
||
tui_available() {
|
||
[ "${BETTERDESK_CLASSIC_MENU:-0}" = "1" ] && return 1
|
||
[ -t 0 ] && [ -t 1 ] || return 1
|
||
return 0
|
||
}
|
||
|
||
# Cleanup helper: always restore the cursor when leaving the TUI.
|
||
_tui_restore() { printf '\033[?25h' 2>/dev/null; stty echo 2>/dev/null; }
|
||
|
||
# tui_select "Title" "Subtitle" item1 item2 ...
|
||
# Each item may embed a description after a literal $'\t' (tab).
|
||
# Navigation: ↑/↓ or k/j to move, Enter/→ to choose, q/Esc/0 to cancel.
|
||
tui_select() {
|
||
local title="$1"; shift
|
||
local subtitle="$1"; shift
|
||
local items=("$@")
|
||
local count=${#items[@]}
|
||
local sel=0 key rest
|
||
|
||
if ! tui_available || [ "$count" -eq 0 ]; then
|
||
TUI_RESULT=""
|
||
return 1
|
||
fi
|
||
|
||
printf '\033[?25l' # hide cursor
|
||
trap '_tui_restore' INT TERM
|
||
|
||
clear
|
||
while true; do
|
||
# Build the whole frame in a single buffer, then emit it with one
|
||
# write. Terminals (notably the VS Code integrated terminal with GPU
|
||
# acceleration) drop individual glyphs when a full-screen TUI is redrawn
|
||
# via many separate printf calls after each keypress. One write avoids it.
|
||
local buf=""
|
||
buf+="\033[H" # move cursor home instead of clearing (less flicker)
|
||
buf+="${CYAN}${BOLD}+--------------------------------------------------------------+${NC}\033[K\n"
|
||
buf+="$(printf "${CYAN}${BOLD}|${NC} ${WHITE}${BOLD}%-60s${NC} ${CYAN}${BOLD}|${NC}" "$title")\033[K\n"
|
||
if [ -n "$subtitle" ]; then
|
||
buf+="$(printf "${CYAN}${BOLD}|${NC} ${DIM}%-60s${NC} ${CYAN}${BOLD}|${NC}" "$subtitle")\033[K\n"
|
||
fi
|
||
buf+="${CYAN}${BOLD}+--------------------------------------------------------------+${NC}\033[K\n"
|
||
buf+="\033[K\n"
|
||
|
||
local i label desc pad line
|
||
for i in "${!items[@]}"; do
|
||
label="${items[$i]%%$'\t'*}"
|
||
desc=""
|
||
[[ "${items[$i]}" == *$'\t'* ]] && desc="${items[$i]#*$'\t'}"
|
||
# Manual padding by character count keeps columns aligned reliably.
|
||
pad=$(( 32 - ${#label} ))
|
||
[ "$pad" -lt 1 ] && pad=1
|
||
if [ "$i" -eq "$sel" ]; then
|
||
line="$(printf " ${GREEN}${BOLD}>${NC} ${GREEN}${BOLD}%s${NC}%*s${DIM}%s${NC}" "$label" "$pad" "" "$desc")"
|
||
else
|
||
line="$(printf " ${WHITE}%s${NC}%*s${DIM}%s${NC}" "$label" "$pad" "" "$desc")"
|
||
fi
|
||
buf+="${line}\033[K\n"
|
||
done
|
||
|
||
buf+="\033[K\n"
|
||
buf+=" ${DIM}Up/Down navigate Enter select q/Esc back${NC}\033[K\n"
|
||
buf+="\033[J" # clear anything below the menu
|
||
|
||
printf '%b' "$buf"
|
||
|
||
# Read a single keypress (with escape-sequence handling)
|
||
IFS= read -rsn1 key 2>/dev/null
|
||
if [[ "$key" == $'\033' ]]; then
|
||
read -rsn2 -t 0.05 rest 2>/dev/null
|
||
key+="$rest"
|
||
fi
|
||
|
||
case "$key" in
|
||
$'\033[A'|'k') sel=$(( (sel - 1 + count) % count )) ;;
|
||
$'\033[B'|'j') sel=$(( (sel + 1) % count )) ;;
|
||
''|$'\033[C') TUI_RESULT="$sel"; _tui_restore; trap - INT TERM; return 0 ;; # Enter / →
|
||
'q'|'Q'|'0'|$'\033') TUI_RESULT=""; _tui_restore; trap - INT TERM; return 2 ;;
|
||
[1-9])
|
||
# Numeric shortcut jumps straight to that 1-based entry
|
||
local idx=$(( key - 1 ))
|
||
if [ "$idx" -lt "$count" ]; then
|
||
TUI_RESULT="$idx"; _tui_restore; trap - INT TERM; return 0
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
#===============================================================================
|
||
# Modern UI helpers shared by every sub-menu
|
||
#===============================================================================
|
||
# ui_panel_header "Title" "Subtitle"
|
||
# Draws a clean ASCII box header (single buffered write to avoid glyph drops).
|
||
ui_panel_header() {
|
||
local title="$1" subtitle="$2"
|
||
clear 2>/dev/null || true
|
||
local buf=""
|
||
buf+="${CYAN}${BOLD}+--------------------------------------------------------------+${NC}\n"
|
||
buf+="$(printf "${CYAN}${BOLD}|${NC} ${WHITE}${BOLD}%-60s${NC} ${CYAN}${BOLD}|${NC}" "$title")\n"
|
||
if [ -n "$subtitle" ]; then
|
||
buf+="$(printf "${CYAN}${BOLD}|${NC} ${DIM}%-60s${NC} ${CYAN}${BOLD}|${NC}" "$subtitle")\n"
|
||
fi
|
||
buf+="${CYAN}${BOLD}+--------------------------------------------------------------+${NC}\n"
|
||
printf '%b' "$buf"
|
||
echo ""
|
||
}
|
||
|
||
# ui_section "Title" — a lightweight section divider for output screens.
|
||
ui_section() {
|
||
echo ""
|
||
echo -e " ${CYAN}${BOLD}== $1 ==${NC}"
|
||
echo ""
|
||
}
|
||
|
||
# menu_choose "Title" "Subtitle"
|
||
# Caller must pre-populate two parallel arrays:
|
||
# _menu_items=( "Label\tDescription" ... ) # what the user sees
|
||
# _menu_returns=( "1" "2" ... "0" ) # value returned for each entry
|
||
# The chosen value is stored in MENU_CHOICE. On cancel (q/Esc) the LAST entry's
|
||
# value is returned (by convention the final item is "Back"/"Exit").
|
||
# Uses the arrow-key TUI when available, otherwise a styled numeric menu.
|
||
MENU_CHOICE=""
|
||
menu_choose() {
|
||
local title="$1" subtitle="$2"
|
||
MENU_CHOICE=""
|
||
local last_idx=$(( ${#_menu_returns[@]} - 1 ))
|
||
[ "$last_idx" -lt 0 ] && last_idx=0
|
||
|
||
if tui_available; then
|
||
tui_select "$title" "$subtitle" "${_menu_items[@]}"
|
||
local rc=$?
|
||
if [ "$rc" -eq 0 ] && [ -n "$TUI_RESULT" ]; then
|
||
MENU_CHOICE="${_menu_returns[$TUI_RESULT]}"
|
||
else
|
||
MENU_CHOICE="${_menu_returns[$last_idx]}"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# Styled numeric fallback (no TTY / classic mode)
|
||
ui_panel_header "$title" "$subtitle"
|
||
local i label desc
|
||
for i in "${!_menu_items[@]}"; do
|
||
label="${_menu_items[$i]%%$'\t'*}"
|
||
desc=""
|
||
[[ "${_menu_items[$i]}" == *$'\t'* ]] && desc="${_menu_items[$i]#*$'\t'}"
|
||
printf " ${GREEN}${BOLD}%2s${NC}) ${WHITE}%-28s${NC} ${DIM}%s${NC}\n" \
|
||
"${_menu_returns[$i]}" "$label" "$desc"
|
||
done
|
||
echo ""
|
||
echo -ne " ${CYAN}Select option:${NC} "
|
||
read -r MENU_CHOICE
|
||
}
|
||
|
||
get_public_ip() {
|
||
local ip
|
||
ip=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null) && [ -n "$ip" ] && echo "$ip" && return
|
||
ip=$(curl -4 -s --max-time 5 icanhazip.com 2>/dev/null) && [ -n "$ip" ] && echo "$ip" && return
|
||
ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) && [ -n "$ip" ] && echo "$ip" && return
|
||
ip=$(curl -s --max-time 5 icanhazip.com 2>/dev/null) && [ -n "$ip" ] && echo "$ip" && return
|
||
echo "127.0.0.1"
|
||
}
|
||
|
||
# Detect the server's primary LAN/private IPv4 address.
|
||
# Used for LAN-only deployments where the public IP is unreachable by clients.
|
||
get_local_ip() {
|
||
local ip
|
||
# Primary: source address used to reach the default gateway
|
||
ip=$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[0-9.]+' | head -1)
|
||
[ -n "$ip" ] && echo "$ip" && return
|
||
# Fallback: first non-loopback global-scope address
|
||
ip=$(ip -4 addr show scope global 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1)
|
||
[ -n "$ip" ] && echo "$ip" && return
|
||
# Last resort: hostname resolution
|
||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
[ -n "$ip" ] && echo "$ip" && return
|
||
echo "127.0.0.1"
|
||
}
|
||
|
||
# Resolve P2P/relay connection strategy env vars for systemd/NSSM (issue #157).
|
||
resolve_connection_mode_env() {
|
||
CONNECTION_MODE="${CONNECTION_MODE:-p2p_first}"
|
||
if [ "$AUTO_MODE" = false ] && [ -z "$CONNECTION_MODE_SET" ]; then
|
||
echo ""
|
||
print_info "Connection strategy: P2P hole punch vs relay-only routing."
|
||
echo -e " ${CYAN}1)${NC} P2P first (recommended) — try direct connection, fall back to relay"
|
||
echo -e " ${CYAN}2)${NC} Relay only — route all sessions through the relay server"
|
||
echo -ne " ${CYAN}Select connection mode [1]:${NC} "
|
||
read -r _conn_choice
|
||
case "$_conn_choice" in
|
||
2) CONNECTION_MODE="relay_only" ;;
|
||
*) CONNECTION_MODE="p2p_first" ;;
|
||
esac
|
||
echo ""
|
||
fi
|
||
|
||
P2P_FALLBACK_MS="${P2P_FALLBACK_MS:-2000}"
|
||
SAME_NAT_RELAY="${SAME_NAT_RELAY:-Y}"
|
||
|
||
case "$CONNECTION_MODE" in
|
||
relay_only)
|
||
P2P_FIRST_ENV=N
|
||
ALWAYS_USE_RELAY_ENV=Y
|
||
;;
|
||
*)
|
||
P2P_FIRST_ENV=Y
|
||
ALWAYS_USE_RELAY_ENV=N
|
||
;;
|
||
esac
|
||
|
||
CONNECTION_MODE_ENV_BLOCK="Environment=P2P_FIRST=$P2P_FIRST_ENV
|
||
Environment=ALWAYS_USE_RELAY=$ALWAYS_USE_RELAY_ENV
|
||
Environment=P2P_FALLBACK_MS=$P2P_FALLBACK_MS
|
||
Environment=SAME_NAT_RELAY=$SAME_NAT_RELAY"
|
||
print_info "Connection mode: ${CONNECTION_MODE} (P2P_FIRST=$P2P_FIRST_ENV, ALWAYS_USE_RELAY=$ALWAYS_USE_RELAY_ENV)"
|
||
}
|
||
|
||
# Resolve the relay server address according to RELAY_MODE / RELAY_SERVERS.
|
||
# Prints the resolved address to stdout; warnings/info go to stderr so the
|
||
# captured value (server_ip=$(resolve_relay_ip)) stays clean.
|
||
resolve_relay_ip() {
|
||
# Explicit override always wins
|
||
if [ -n "$RELAY_SERVERS" ]; then
|
||
echo "Using fixed relay address (RELAY_SERVERS): $RELAY_SERVERS" >&2
|
||
echo "$RELAY_SERVERS"
|
||
return
|
||
fi
|
||
|
||
local ip
|
||
case "${RELAY_MODE:-auto}" in
|
||
local|lan)
|
||
ip=$(get_local_ip)
|
||
echo "Relay mode 'local': using LAN IP $ip (LAN-only deployment)" >&2
|
||
;;
|
||
public|wan)
|
||
ip=$(get_public_ip)
|
||
echo "Relay mode 'public': using public IP $ip" >&2
|
||
;;
|
||
auto|*)
|
||
ip=$(get_public_ip)
|
||
# Warn if auto-detection returned a private/loopback address — relay
|
||
# will not work for remote clients unless this is a LAN-only setup.
|
||
if [ "$ip" = "127.0.0.1" ] || [[ "$ip" == 10.* ]] || [[ "$ip" == 192.168.* ]] || [[ "$ip" == 172.1[6-9].* ]] || [[ "$ip" == 172.2[0-9].* ]] || [[ "$ip" == 172.3[0-1].* ]]; then
|
||
echo "WARNING: Auto-detected private/loopback IP: $ip" >&2
|
||
echo "WARNING: Remote (internet) clients will NOT connect via relay with this address." >&2
|
||
echo " For LAN-only use, this is fine. For internet access, run with:" >&2
|
||
echo " --relay-servers YOUR.PUBLIC.IP (or RELAY_SERVERS env var)" >&2
|
||
echo " To silence this and use the LAN IP explicitly, run with:" >&2
|
||
echo " --relay-mode local" >&2
|
||
fi
|
||
;;
|
||
esac
|
||
echo "$ip"
|
||
}
|
||
|
||
sql_escape_literal() {
|
||
# Escape single quotes for SQL string literals: ' -> ''
|
||
local value="$1"
|
||
printf "%s" "${value//\'/\'\'}"
|
||
}
|
||
|
||
is_valid_pg_identifier() {
|
||
# PostgreSQL unquoted identifier compatible pattern.
|
||
# Keeps installation scripts safe from SQL injection in CREATE/ALTER statements.
|
||
local ident="$1"
|
||
[[ "$ident" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]]
|
||
}
|
||
|
||
#===============================================================================
|
||
# Service Management Functions (Enhanced v2.1.2)
|
||
#===============================================================================
|
||
|
||
# Wait for a service to fully stop with timeout
|
||
wait_for_service_stop() {
|
||
local service_name="$1"
|
||
local timeout="${2:-30}"
|
||
local elapsed=0
|
||
|
||
while [ $elapsed -lt $timeout ]; do
|
||
if ! systemctl is-active --quiet "$service_name" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
|
||
print_warning "Service $service_name did not stop within ${timeout}s"
|
||
return 1
|
||
}
|
||
|
||
# Kill any stale processes that might be holding files/ports
|
||
# Free BetterDesk ports when systemd stop left orphan listeners (#219).
|
||
kill_processes_holding_ports() {
|
||
local port pids
|
||
for port in 21116 21117 5000 5443; do
|
||
if ! ss -tlnH 2>/dev/null | grep -q ":${port} "; then
|
||
continue
|
||
fi
|
||
pids=$(lsof -t -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)
|
||
if [ -n "$pids" ]; then
|
||
print_warning "Port ${port} still in use (pids: $pids) — terminating (#219)"
|
||
for pid in $pids; do
|
||
kill -TERM "$pid" 2>/dev/null || true
|
||
done
|
||
sleep 1
|
||
pids=$(lsof -t -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)
|
||
if [ -n "$pids" ]; then
|
||
for pid in $pids; do
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
done
|
||
fi
|
||
elif command -v fuser &>/dev/null; then
|
||
fuser -k "${port}/tcp" 2>/dev/null || true
|
||
fi
|
||
done
|
||
sleep 1
|
||
}
|
||
|
||
kill_stale_processes() {
|
||
local process_name="$1"
|
||
|
||
# Find and kill any remaining processes
|
||
local pids=$(pgrep -f "$process_name" 2>/dev/null || true)
|
||
|
||
if [ -n "$pids" ]; then
|
||
print_warning "Found stale $process_name processes: $pids"
|
||
|
||
# Try graceful termination first
|
||
for pid in $pids; do
|
||
kill -TERM "$pid" 2>/dev/null || true
|
||
done
|
||
sleep 2
|
||
|
||
# Force kill if still running
|
||
pids=$(pgrep -f "$process_name" 2>/dev/null || true)
|
||
if [ -n "$pids" ]; then
|
||
for pid in $pids; do
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
done
|
||
sleep 1
|
||
fi
|
||
|
||
print_info "Cleaned up stale $process_name processes"
|
||
fi
|
||
}
|
||
|
||
# Check if a port is available
|
||
check_port_available() {
|
||
local port="$1"
|
||
local service_name="${2:-unknown}"
|
||
|
||
if ss -tlnp 2>/dev/null | grep -q ":${port} " || \
|
||
netstat -tlnp 2>/dev/null | grep -q ":${port} "; then
|
||
local process=$(ss -tlnp 2>/dev/null | grep ":${port} " | awk '{print $NF}' || \
|
||
netstat -tlnp 2>/dev/null | grep ":${port} " | awk '{print $NF}')
|
||
print_error "Port $port is already in use by: $process"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Verify that a service is healthy (running and listening on expected port)
|
||
_tcp_port_is_listening() {
|
||
local port="$1"
|
||
# Match :PORT followed by whitespace or end — covers ss/netstat layouts
|
||
# like "0.0.0.0:80", "*:80", "[::]:80" (#219).
|
||
ss -tlnH 2>/dev/null | grep -qE ":${port}([[:space:]]|$)" || \
|
||
ss -tln 2>/dev/null | grep -qE ":${port}([[:space:]]|$)" || \
|
||
netstat -tln 2>/dev/null | grep -qE ":${port}([[:space:]]|$)"
|
||
}
|
||
|
||
# Hint when .env requests a privileged panel port but Node bound a fallback (#219).
|
||
_hint_panel_privileged_port_mismatch() {
|
||
local expected_port="$1"
|
||
local https_enabled
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" != "true" ]; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$expected_port" = "443" ] && _tcp_port_is_listening 5443; then
|
||
print_info " Panel is listening on :5443 instead of configured :443"
|
||
print_info " → Run Repair → Repair permissions (adds CAP_NET_BIND_SERVICE), then restart betterdesk-console"
|
||
print_info " → Or set HTTPS_PORT=5443 and use a reverse proxy on :443 (docs/setup/REVERSE_PROXY.md)"
|
||
return 0
|
||
fi
|
||
|
||
if [ "$expected_port" = "80" ] && _tcp_port_is_listening 5000; then
|
||
print_info " HTTP redirect is on :5000 instead of configured :80 — set PORT=80 and run Repair → Repair permissions"
|
||
fi
|
||
}
|
||
|
||
verify_service_health() {
|
||
local service_name="$1"
|
||
local expected_port="$2"
|
||
local timeout="${3:-10}"
|
||
local elapsed=0
|
||
|
||
# First check if service is active
|
||
if ! systemctl is-active --quiet "$service_name" 2>/dev/null; then
|
||
print_error "Service $service_name is not running"
|
||
show_service_logs "$service_name" 20
|
||
return 1
|
||
fi
|
||
|
||
# If port specified, wait for it to be bound
|
||
if [ -n "$expected_port" ]; then
|
||
while [ $elapsed -lt $timeout ]; do
|
||
if _tcp_port_is_listening "$expected_port"; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
|
||
print_error "Service $service_name is running but not listening on port $expected_port"
|
||
if [ "$service_name" = "betterdesk-console" ]; then
|
||
_hint_panel_privileged_port_mismatch "$expected_port"
|
||
fi
|
||
show_service_logs "$service_name" 20
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# Show recent service logs for debugging
|
||
show_service_logs() {
|
||
local service_name="$1"
|
||
local lines="${2:-30}"
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}═══ Recent logs for $service_name ═══${NC}"
|
||
journalctl -u "$service_name" -n "$lines" --no-pager 2>/dev/null || \
|
||
print_warning "Could not retrieve logs for $service_name"
|
||
echo -e "${YELLOW}═══════════════════════════════════════${NC}"
|
||
echo ""
|
||
}
|
||
|
||
# Gracefully stop all BetterDesk services with proper cleanup
|
||
graceful_stop_services() {
|
||
print_step "Stopping services gracefully..."
|
||
|
||
# New Go services (primary)
|
||
local services=("betterdesk-console" "betterdesk-server")
|
||
# Legacy services (for migration)
|
||
local legacy_services=("betterdesk" "rustdesksignal" "rustdeskrelay" "betterdesk-api" "betterdesk-go")
|
||
|
||
# Stop current services
|
||
for service in "${services[@]}"; do
|
||
if systemctl is-active --quiet "$service" 2>/dev/null; then
|
||
print_info "Stopping $service..."
|
||
systemctl stop "$service" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# Stop legacy services if they exist
|
||
for service in "${legacy_services[@]}"; do
|
||
if systemctl is-active --quiet "$service" 2>/dev/null; then
|
||
print_info "Stopping legacy $service..."
|
||
systemctl stop "$service" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# Wait for services to stop
|
||
for service in "${services[@]}" "${legacy_services[@]}"; do
|
||
wait_for_service_stop "$service" 15
|
||
done
|
||
|
||
# Kill any stale processes (Go and legacy Rust)
|
||
kill_stale_processes "betterdesk-server"
|
||
kill_stale_processes "hbbs"
|
||
kill_stale_processes "hbbr"
|
||
kill_processes_holding_ports
|
||
|
||
print_success "All services stopped"
|
||
}
|
||
|
||
# Read console setting with systemd Environment= overriding .env (matches runtime order).
|
||
# Prefer .env over unit Environment= — matches systemd EnvironmentFile= precedence
|
||
# (EnvironmentFile overrides Environment=). Stale Environment=PORT=5000 must not
|
||
# win over .env PORT=80 when probing redirect / panel ports (#219).
|
||
read_effective_console_setting() {
|
||
local key="$1"
|
||
local default="${2:-}"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local val=""
|
||
|
||
if [ -f "$env_file" ]; then
|
||
val=$(grep -m1 "^${key}=" "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
|
||
fi
|
||
if [ -z "$val" ] && [ -f "$svc_file" ]; then
|
||
val=$(grep -E "^Environment=${key}=" "$svc_file" 2>/dev/null | tail -1 | sed "s/^Environment=${key}=//")
|
||
fi
|
||
if [ -z "$val" ]; then
|
||
val="$default"
|
||
fi
|
||
echo "$val"
|
||
}
|
||
|
||
# Keep betterdesk-console.service Environment=PORT/HTTPS_PORT aligned with .env (#219).
|
||
_sync_console_panel_ports_to_systemd() {
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local http_port https_port changed=0
|
||
|
||
[ -f "$svc_file" ] || return 1
|
||
|
||
http_port=$(grep -m1 '^PORT=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
|
||
https_port=$(grep -m1 '^HTTPS_PORT=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
|
||
[ -n "$http_port" ] || http_port="5000"
|
||
[ -n "$https_port" ] || https_port="5443"
|
||
|
||
if ! grep -qE "^Environment=PORT=${http_port}$" "$svc_file" 2>/dev/null; then
|
||
_upsert_systemd_env "$svc_file" PORT "$http_port"
|
||
changed=1
|
||
fi
|
||
if [ "$(read_effective_console_setting HTTPS_ENABLED false | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
if ! grep -qE "^Environment=HTTPS_PORT=${https_port}$" "$svc_file" 2>/dev/null; then
|
||
_upsert_systemd_env "$svc_file" HTTPS_PORT "$https_port"
|
||
changed=1
|
||
fi
|
||
fi
|
||
|
||
if [ "$changed" -eq 1 ]; then
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
_upsert_env_line() {
|
||
local file="$1" key="$2" value="$3"
|
||
[ -f "$file" ] || touch "$file"
|
||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
|
||
else
|
||
echo "${key}=${value}" >> "$file"
|
||
fi
|
||
}
|
||
|
||
_remove_env_line() {
|
||
local file="$1" key="$2"
|
||
[ -f "$file" ] && sed -i "/^${key}=/d" "$file"
|
||
}
|
||
|
||
_upsert_systemd_env() {
|
||
local file="$1" key="$2" value="$3"
|
||
[ -f "$file" ] || return 0
|
||
if grep -q "Environment=${key}=" "$file" 2>/dev/null; then
|
||
sed -i "s|Environment=${key}=.*|Environment=${key}=${value}|" "$file"
|
||
else
|
||
sed -i "/^\[Service\]/a Environment=${key}=${value}" "$file"
|
||
fi
|
||
}
|
||
|
||
_remove_systemd_env() {
|
||
local file="$1" key="$2"
|
||
[ -f "$file" ] && sed -i "/Environment=${key}=/d" "$file"
|
||
}
|
||
|
||
# Infer RUSTDESK_API_TLS / ALLOW_SELF_SIGNED_CERTS from certificate path (LE vs self-signed).
|
||
infer_tls_mode_from_cert() {
|
||
local cert_path="$1"
|
||
local resolved
|
||
resolved=$(readlink -f "$cert_path" 2>/dev/null || echo "$cert_path")
|
||
if [[ "$resolved" == *"/etc/letsencrypt/"* ]]; then
|
||
INFERRED_RUSTDESK_API_TLS="true"
|
||
INFERRED_ALLOW_SELF_SIGNED="false"
|
||
else
|
||
INFERRED_RUSTDESK_API_TLS="false"
|
||
INFERRED_ALLOW_SELF_SIGNED="true"
|
||
fi
|
||
}
|
||
|
||
# Resolve Let's Encrypt live/ dir from .env, paths, symlinks, LE_CERT_DOMAIN, or cert SAN (#219).
|
||
resolve_le_cert_live_dir() {
|
||
local env_file="${1:-${CONSOLE_PATH}/.env}"
|
||
local cert_hint="${2:-$RUSTDESK_PATH/ssl/betterdesk.crt}"
|
||
local ssl_key_env ssl_cert_env le_live_dir le_domain dns_name
|
||
|
||
le_live_dir=$(grep -m1 '^LE_CERT_LIVE_DIR=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
ssl_cert_env=$(grep -m1 '^SSL_CERT_PATH=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
ssl_key_env=$(grep -m1 '^SSL_KEY_PATH=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
|
||
if [ -z "$le_live_dir" ] && [[ "$ssl_cert_env" == *"/etc/letsencrypt/"* ]]; then
|
||
le_live_dir=$(dirname "$(readlink -f "$ssl_cert_env" 2>/dev/null || echo "$ssl_cert_env")")
|
||
fi
|
||
if [ -z "$le_live_dir" ] && [[ "$ssl_key_env" == *"/etc/letsencrypt/"* ]]; then
|
||
le_live_dir=$(dirname "$(readlink -f "$ssl_key_env" 2>/dev/null || echo "$ssl_key_env")")
|
||
fi
|
||
if [ -z "$le_live_dir" ] && [ -L "$cert_hint" ]; then
|
||
le_live_dir=$(dirname "$(readlink -f "$cert_hint" 2>/dev/null || echo "")")
|
||
fi
|
||
if [ -z "$le_live_dir" ]; then
|
||
le_domain=$(grep -m1 '^LE_CERT_DOMAIN=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
if [ -n "$le_domain" ] && [ -d "/etc/letsencrypt/live/$le_domain" ]; then
|
||
le_live_dir="/etc/letsencrypt/live/$le_domain"
|
||
fi
|
||
fi
|
||
if [ -z "$le_live_dir" ] && [ -f "$cert_hint" ] && command -v openssl &>/dev/null; then
|
||
dns_name=$(openssl x509 -in "$cert_hint" -noout -ext subjectAltName 2>/dev/null | \
|
||
grep -oE 'DNS:[^, ]+' | head -1 | cut -d: -f2- || true)
|
||
if [ -n "$dns_name" ] && [ -d "/etc/letsencrypt/live/$dns_name" ]; then
|
||
le_live_dir="/etc/letsencrypt/live/$dns_name"
|
||
fi
|
||
fi
|
||
echo "$le_live_dir"
|
||
}
|
||
|
||
# Copy a TLS file to dest as a real file (not a symlink).
|
||
# - If dest is already the same real file as src (self-signed generated in place),
|
||
# do nothing — deleting dest would remove src and break cp (#325, discussion #322).
|
||
# - If dest is a symlink that resolves to src (LE live dir), remove the symlink
|
||
# and copy content so the console user can read it (#219).
|
||
_safe_cp_tls_file() {
|
||
local src="$1"
|
||
local dest="$2"
|
||
local src_real dest_real tmp
|
||
|
||
[ -f "$src" ] || return 1
|
||
src_real=$(readlink -f "$src" 2>/dev/null || echo "$src")
|
||
if [ -e "$dest" ]; then
|
||
dest_real=$(readlink -f "$dest" 2>/dev/null || echo "$dest")
|
||
if [ "$src_real" = "$dest_real" ]; then
|
||
if [ -L "$dest" ]; then
|
||
# Symlink to src → replace with a real copy (#219)
|
||
rm -f "$dest"
|
||
else
|
||
# Already a real file at dest (self-signed path) — no copy needed (#325)
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
tmp="${dest}.betterdesk.$$.tmp"
|
||
cp -L "$src" "$tmp" || return 1
|
||
mv -f "$tmp" "$dest" || { rm -f "$tmp"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
# Ensure Go signal/relay/API ports are not overridden by shared .env (#219).
|
||
ensure_go_server_signal_ports() {
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
local changed=0
|
||
local go_api_port="${GO_API_PORT:-21114}"
|
||
|
||
[ -f "$go_svc_file" ] || return 1
|
||
|
||
if ! grep -q '^Environment=SIGNAL_PORT=21116' "$go_svc_file" 2>/dev/null; then
|
||
_upsert_systemd_env "$go_svc_file" SIGNAL_PORT 21116
|
||
changed=1
|
||
fi
|
||
if ! grep -q '^Environment=RELAY_PORT=21117' "$go_svc_file" 2>/dev/null; then
|
||
_upsert_systemd_env "$go_svc_file" RELAY_PORT 21117
|
||
changed=1
|
||
fi
|
||
if ! grep -q "^Environment=GO_API_PORT=${go_api_port}" "$go_svc_file" 2>/dev/null; then
|
||
_upsert_systemd_env "$go_svc_file" GO_API_PORT "$go_api_port"
|
||
changed=1
|
||
fi
|
||
if [ -f "$go_svc_file" ] && grep -qE '\-api-port[[:space:]]+21121\b' "$go_svc_file" 2>/dev/null; then
|
||
print_info "Migrating Go -api-port 21121 → ${go_api_port} (handlers on Go; clients stay on :${CLIENT_API_PORT:-21121} proxy)"
|
||
sed -i "s/-api-port 21121/-api-port ${go_api_port}/" "$go_svc_file"
|
||
changed=1
|
||
fi
|
||
if [ "$changed" -eq 1 ]; then
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
print_info "Go server uses SIGNAL_PORT=21116, GO_API_PORT=${go_api_port} (panel PORT/API_PORT in .env are console-only)"
|
||
fi
|
||
[ "$changed" -eq 1 ]
|
||
}
|
||
|
||
# Copy TLS material into $RUSTDESK_PATH/ssl/ as real files (not symlinks) so the
|
||
# betterdesk console user can read them. LE live dirs are root-only (#219).
|
||
deploy_ssl_material_to_rustdesk_dir() {
|
||
local cert_src="$1"
|
||
local key_src="$2"
|
||
local le_live_dir="${3:-}"
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local svc_user="betterdesk"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
|
||
if [ ! -f "$cert_src" ] || [ ! -f "$key_src" ]; then
|
||
print_error "Certificate or key source not found: cert=$cert_src key=$key_src"
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p "$ssl_dir"
|
||
if ! _safe_cp_tls_file "$cert_src" "$ssl_dir/betterdesk.crt"; then
|
||
print_error "Failed to deploy certificate to $ssl_dir/betterdesk.crt"
|
||
return 1
|
||
fi
|
||
if ! _safe_cp_tls_file "$key_src" "$ssl_dir/betterdesk.key"; then
|
||
print_error "Failed to deploy private key to $ssl_dir/betterdesk.key"
|
||
return 1
|
||
fi
|
||
|
||
if id "$svc_user" &>/dev/null; then
|
||
chown root:"$svc_user" "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" 2>/dev/null || true
|
||
fi
|
||
chmod 640 "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" 2>/dev/null || true
|
||
|
||
if [ -n "$le_live_dir" ]; then
|
||
_upsert_env_line "$env_file" LE_CERT_LIVE_DIR "$le_live_dir"
|
||
_upsert_env_line "$env_file" LE_CERT_DOMAIN "$(basename "$le_live_dir")"
|
||
install_le_certbot_renew_hook
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# certbot deploy hook: re-copy renewed LE certs then restart BetterDesk services.
|
||
install_le_certbot_renew_hook() {
|
||
local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
|
||
local hook="$hook_dir/betterdesk-reload.sh"
|
||
local conf="$hook_dir/betterdesk-reload.conf"
|
||
mkdir -p "$hook_dir"
|
||
|
||
cat > "$conf" <<EOF
|
||
RUSTDESK_PATH=${RUSTDESK_PATH}
|
||
CONSOLE_PATH=${CONSOLE_PATH}
|
||
EOF
|
||
chmod 640 "$conf"
|
||
|
||
cat > "$hook" <<'HOOK'
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
CONF="/etc/letsencrypt/renewal-hooks/deploy/betterdesk-reload.conf"
|
||
[ -f "$CONF" ] && . "$CONF"
|
||
RUSTDESK_PATH="${RUSTDESK_PATH:-/opt/rustdesk}"
|
||
CONSOLE_PATH="${CONSOLE_PATH:-/opt/betterdesk}"
|
||
ENV_FILE="$CONSOLE_PATH/.env"
|
||
SSL_DIR="$RUSTDESK_PATH/ssl"
|
||
SVC_USER="betterdesk"
|
||
|
||
le_live_dir=""
|
||
if [ -f "$ENV_FILE" ]; then
|
||
le_live_dir=$(grep -m1 '^LE_CERT_LIVE_DIR=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
|
||
fi
|
||
if [ -z "$le_live_dir" ] || [ ! -d "$le_live_dir" ]; then
|
||
echo "betterdesk-reload: LE_CERT_LIVE_DIR missing or invalid — skipping cert copy" >&2
|
||
systemctl restart betterdesk-server betterdesk-console 2>/dev/null || true
|
||
exit 0
|
||
fi
|
||
|
||
mkdir -p "$SSL_DIR"
|
||
for pair in "fullchain.pem:betterdesk.crt" "privkey.pem:betterdesk.key"; do
|
||
src_name="${pair%%:*}"
|
||
dest_name="${pair##*:}"
|
||
src="$le_live_dir/$src_name"
|
||
dest="$SSL_DIR/$dest_name"
|
||
src_real=$(readlink -f "$src" 2>/dev/null || echo "$src")
|
||
if [ -e "$dest" ]; then
|
||
dest_real=$(readlink -f "$dest" 2>/dev/null || echo "$dest")
|
||
[ "$src_real" = "$dest_real" ] && rm -f "$dest"
|
||
fi
|
||
tmp="${dest}.betterdesk.$$.tmp"
|
||
cp -L "$src" "$tmp"
|
||
mv -f "$tmp" "$dest"
|
||
done
|
||
if id "$SVC_USER" &>/dev/null; then
|
||
chown root:"$SVC_USER" "$SSL_DIR/betterdesk.crt" "$SSL_DIR/betterdesk.key"
|
||
fi
|
||
chmod 640 "$SSL_DIR/betterdesk.crt" "$SSL_DIR/betterdesk.key"
|
||
systemctl restart betterdesk-server betterdesk-console 2>/dev/null || true
|
||
HOOK
|
||
chmod +x "$hook"
|
||
}
|
||
|
||
# Repair installs that symlinked LE certs into ssl/ (console user cannot read privkey).
|
||
maybe_repair_le_ssl_symlinks() {
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local crt="$ssl_dir/betterdesk.crt"
|
||
local key="$ssl_dir/betterdesk.key"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local needs_redeploy="no"
|
||
local ssl_key_env ssl_cert_env
|
||
local console_user="betterdesk"
|
||
|
||
for f in "$crt" "$key"; do
|
||
if [ -L "$f" ]; then
|
||
local target
|
||
target=$(readlink -f "$f" 2>/dev/null || readlink "$f" 2>/dev/null || echo "")
|
||
if [[ "$target" == *"/etc/letsencrypt/"* ]]; then
|
||
needs_redeploy="yes"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
ssl_key_env=$(grep -m1 '^SSL_KEY_PATH=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
ssl_cert_env=$(grep -m1 '^SSL_CERT_PATH=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
|
||
if [[ "$ssl_key_env" == *"/etc/letsencrypt/"* ]] || [[ "$ssl_cert_env" == *"/etc/letsencrypt/"* ]]; then
|
||
needs_redeploy="yes"
|
||
fi
|
||
if id "$console_user" &>/dev/null && [ -e "$key" ] \
|
||
&& ! runuser -u "$console_user" -- test -r "$key" 2>/dev/null; then
|
||
needs_redeploy="yes"
|
||
fi
|
||
|
||
if [ "$needs_redeploy" != "yes" ]; then
|
||
return 2
|
||
fi
|
||
|
||
local le_live_dir
|
||
le_live_dir=$(resolve_le_cert_live_dir "$env_file" "$crt")
|
||
|
||
if [ -z "$le_live_dir" ] || [ ! -f "$le_live_dir/fullchain.pem" ] || [ ! -f "$le_live_dir/privkey.pem" ]; then
|
||
print_warning "LE certificate symlinks detected but live dir not found — manual repair may be needed"
|
||
return 1
|
||
fi
|
||
|
||
print_info "Repairing LE certificate symlinks → copied files for console user (#219)"
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$le_live_dir/fullchain.pem" "$le_live_dir/privkey.pem" "$le_live_dir"; then
|
||
print_error "LE redeploy failed for $le_live_dir — remove symlinks under $ssl_dir and retry"
|
||
return 1
|
||
fi
|
||
_sync_deployed_ssl_paths_to_env
|
||
return 0
|
||
}
|
||
|
||
# When HTTPS uses standard port 443, align HTTP redirect listener to :80 (#219).
|
||
_ensure_standard_https_redirect_ports() {
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local https_port http_port https_enabled changed=0
|
||
local svc_port svc_https
|
||
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" != "true" ]; then
|
||
return 1
|
||
fi
|
||
|
||
https_port=$(read_effective_console_setting HTTPS_PORT 5443)
|
||
[ "$https_port" = "443" ] || return 1
|
||
|
||
http_port=$(read_effective_console_setting PORT 5000)
|
||
if [ "$http_port" != "80" ]; then
|
||
_upsert_env_line "$env_file" PORT 80
|
||
_upsert_env_line "$env_file" HTTP_REDIRECT_HTTPS true
|
||
changed=1
|
||
fi
|
||
if [ -f "$svc_file" ]; then
|
||
svc_port=$(grep -E '^Environment=PORT=' "$svc_file" 2>/dev/null | tail -1 | sed 's/^Environment=PORT=//')
|
||
svc_https=$(grep -E '^Environment=HTTPS_PORT=' "$svc_file" 2>/dev/null | tail -1 | sed 's/^Environment=HTTPS_PORT=//')
|
||
if [ "$svc_port" != "80" ] || [ "$svc_https" != "443" ]; then
|
||
_upsert_systemd_env "$svc_file" HTTPS_PORT 443
|
||
_upsert_systemd_env "$svc_file" PORT 80
|
||
_upsert_systemd_env "$svc_file" HTTP_REDIRECT_HTTPS true
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
changed=1
|
||
fi
|
||
fi
|
||
if [ "$changed" -eq 1 ]; then
|
||
ensure_betterdesk_console_user >/dev/null
|
||
print_info "Standard HTTPS ports synced: HTTPS :443, HTTP redirect :80 (#219)"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# Repair HTTPS stuck state: Go signal port isolation + LE material redeploy (#219).
|
||
repair_https_stuck_state() {
|
||
local quiet="${1:-}"
|
||
local changed=0
|
||
|
||
repair_console_service_user_line "betterdesk"
|
||
|
||
if ensure_go_server_signal_ports; then
|
||
changed=1
|
||
fi
|
||
|
||
local https_enabled
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
local le_rc=2
|
||
maybe_repair_le_ssl_symlinks || le_rc=$?
|
||
if [ "$le_rc" -eq 0 ]; then
|
||
changed=1
|
||
elif [ "$le_rc" -ne 2 ] && ensure_console_tls_material_readable; then
|
||
changed=1
|
||
fi
|
||
_sync_deployed_ssl_paths_to_env 2>/dev/null || true
|
||
if _ensure_standard_https_redirect_ports; then
|
||
changed=1
|
||
fi
|
||
if _sync_console_panel_ports_to_systemd; then
|
||
changed=1
|
||
fi
|
||
fi
|
||
|
||
if [ "$changed" -eq 1 ] && [ "$quiet" != "yes" ]; then
|
||
print_info "HTTPS/TLS configuration repaired (#219)"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Ensure copied TLS files under $RUSTDESK_PATH/ssl/ are referenced in .env + systemd.
|
||
_sync_deployed_ssl_paths_to_env() {
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
|
||
_upsert_env_line "$env_file" SSL_CERT_PATH "$ssl_dir/betterdesk.crt"
|
||
_upsert_env_line "$env_file" SSL_KEY_PATH "$ssl_dir/betterdesk.key"
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" SSL_CERT_PATH "$ssl_dir/betterdesk.crt"
|
||
_upsert_systemd_env "$svc_file" SSL_KEY_PATH "$ssl_dir/betterdesk.key"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# When HTTPS is enabled, ensure the console user can read the TLS private key (#219).
|
||
# Re-copies from LE_CERT_LIVE_DIR when symlinks, unreadable keys, or /etc/letsencrypt paths remain.
|
||
ensure_console_tls_material_readable() {
|
||
local https_enabled console_user="betterdesk"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local ssl_key_path ssl_cert_path
|
||
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" != "true" ]; then
|
||
return 0
|
||
fi
|
||
|
||
maybe_repair_le_ssl_symlinks 2>/dev/null || true
|
||
|
||
ssl_key_path=$(read_effective_console_setting SSL_KEY_PATH "")
|
||
ssl_cert_path=$(read_effective_console_setting SSL_CERT_PATH "")
|
||
if [ -z "$ssl_key_path" ]; then
|
||
ssl_key_path="$ssl_dir/betterdesk.key"
|
||
fi
|
||
|
||
if id "$console_user" &>/dev/null && [ -e "$ssl_key_path" ]; then
|
||
if runuser -u "$console_user" -- test -r "$ssl_key_path" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
local le_live_dir
|
||
le_live_dir=$(resolve_le_cert_live_dir "$env_file" "$ssl_key_path")
|
||
if [ -z "$le_live_dir" ] && [ -n "$ssl_cert_path" ]; then
|
||
le_live_dir=$(resolve_le_cert_live_dir "$env_file" "$ssl_cert_path")
|
||
fi
|
||
|
||
if [ -n "$le_live_dir" ] && [ -f "$le_live_dir/fullchain.pem" ] && [ -f "$le_live_dir/privkey.pem" ]; then
|
||
print_info "Re-deploying Let's Encrypt certificate for console user (#219)"
|
||
if deploy_ssl_material_to_rustdesk_dir "$le_live_dir/fullchain.pem" "$le_live_dir/privkey.pem" "$le_live_dir"; then
|
||
_sync_deployed_ssl_paths_to_env
|
||
if id "$console_user" &>/dev/null && runuser -u "$console_user" -- test -r "$ssl_dir/betterdesk.key" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
print_warning "HTTPS is enabled but console user cannot read TLS key (${ssl_key_path:-$ssl_dir/betterdesk.key})"
|
||
print_info " Check: runuser -u betterdesk -- test -r ${ssl_key_path:-$ssl_dir/betterdesk.key}"
|
||
print_info " Logs: journalctl -u betterdesk-console -n 30 --no-pager"
|
||
return 1
|
||
}
|
||
|
||
# Wait for an HTTP(S) endpoint to return a usable status code (post-restart boot delay).
|
||
_wait_for_http_code() {
|
||
local url="$1"
|
||
local max_wait="${2:-15}"
|
||
local use_insecure="${3:-}"
|
||
local elapsed=0
|
||
local code="000"
|
||
local curl_args=(-s -o /dev/null -w '%{http_code}' --max-time 4)
|
||
|
||
if [ "$use_insecure" = "yes" ]; then
|
||
curl_args=(-k "${curl_args[@]}")
|
||
fi
|
||
|
||
while [ "$elapsed" -lt "$max_wait" ]; do
|
||
code=$(curl "${curl_args[@]}" "$url" 2>/dev/null || echo "000")
|
||
if [[ "$code" =~ ^(200|301|302|304|401|403|405)$ ]]; then
|
||
echo "$code"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
echo "$code"
|
||
return 1
|
||
}
|
||
|
||
# Sync .env + betterdesk-console.service for HTTP or HTTPS panel mode (#219).
|
||
apply_console_protocol_mode() {
|
||
local mode="$1"
|
||
local cert_crt="${2:-}"
|
||
local cert_key="${3:-}"
|
||
local api_tls="${4:-false}"
|
||
local allow_self_signed="${5:-true}"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local go_port="${GO_API_PORT:-21114}"
|
||
|
||
if [ "$mode" = "http" ]; then
|
||
_upsert_env_line "$env_file" HTTPS_ENABLED false
|
||
_upsert_env_line "$env_file" RUSTDESK_API_TLS false
|
||
_upsert_env_line "$env_file" ALLOW_SELF_SIGNED_CERTS false
|
||
_upsert_env_line "$env_file" HTTP_REDIRECT_HTTPS false
|
||
_upsert_env_line "$env_file" TRUST_PROXY false
|
||
_upsert_env_line "$env_file" HBBS_API_URL "http://localhost:${go_port}/api"
|
||
_upsert_env_line "$env_file" BETTERDESK_API_URL "http://localhost:${go_port}/api"
|
||
_remove_env_line "$env_file" NODE_EXTRA_CA_CERTS
|
||
_remove_env_line "$env_file" ENTERPRISE_TLS
|
||
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" HTTPS_ENABLED false
|
||
_upsert_systemd_env "$svc_file" RUSTDESK_API_TLS false
|
||
_upsert_systemd_env "$svc_file" ALLOW_SELF_SIGNED_CERTS false
|
||
_upsert_systemd_env "$svc_file" HTTP_REDIRECT_HTTPS false
|
||
_upsert_systemd_env "$svc_file" TRUST_PROXY false
|
||
sed -i "s|Environment=HBBS_API_URL=https://localhost|Environment=HBBS_API_URL=http://localhost|" "$svc_file"
|
||
sed -i "s|Environment=BETTERDESK_API_URL=https://localhost|Environment=BETTERDESK_API_URL=http://localhost|" "$svc_file"
|
||
_remove_systemd_env "$svc_file" NODE_EXTRA_CA_CERTS
|
||
_remove_systemd_env "$svc_file" ENTERPRISE_TLS
|
||
fi
|
||
sync_go_server_trust_proxy no
|
||
elif [ "$mode" = "https" ]; then
|
||
_upsert_env_line "$env_file" HTTPS_ENABLED true
|
||
_upsert_env_line "$env_file" SSL_CERT_PATH "$cert_crt"
|
||
_upsert_env_line "$env_file" SSL_KEY_PATH "$cert_key"
|
||
_upsert_env_line "$env_file" TRUST_PROXY false
|
||
if ! grep -q '^HTTPS_PORT=' "$env_file" 2>/dev/null; then
|
||
_upsert_env_line "$env_file" HTTPS_PORT 5443
|
||
fi
|
||
_upsert_env_line "$env_file" HTTP_REDIRECT_HTTPS true
|
||
_upsert_env_line "$env_file" RUSTDESK_API_TLS "$api_tls"
|
||
_upsert_env_line "$env_file" ALLOW_SELF_SIGNED_CERTS "$allow_self_signed"
|
||
_upsert_env_line "$env_file" HBBS_API_URL "http://localhost:${go_port}/api"
|
||
_upsert_env_line "$env_file" BETTERDESK_API_URL "http://localhost:${go_port}/api"
|
||
if [ "$allow_self_signed" = "true" ]; then
|
||
_upsert_env_line "$env_file" NODE_EXTRA_CA_CERTS "$cert_crt"
|
||
else
|
||
_remove_env_line "$env_file" NODE_EXTRA_CA_CERTS
|
||
fi
|
||
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" HTTPS_ENABLED true
|
||
_upsert_systemd_env "$svc_file" SSL_CERT_PATH "$cert_crt"
|
||
_upsert_systemd_env "$svc_file" SSL_KEY_PATH "$cert_key"
|
||
_upsert_systemd_env "$svc_file" HTTP_REDIRECT_HTTPS true
|
||
_upsert_systemd_env "$svc_file" RUSTDESK_API_TLS "$api_tls"
|
||
_upsert_systemd_env "$svc_file" ALLOW_SELF_SIGNED_CERTS "$allow_self_signed"
|
||
_upsert_systemd_env "$svc_file" TRUST_PROXY false
|
||
sed -i "s|Environment=HBBS_API_URL=https://localhost|Environment=HBBS_API_URL=http://localhost|" "$svc_file"
|
||
sed -i "s|Environment=BETTERDESK_API_URL=https://localhost|Environment=BETTERDESK_API_URL=http://localhost|" "$svc_file"
|
||
if [ "$allow_self_signed" = "true" ]; then
|
||
_upsert_systemd_env "$svc_file" NODE_EXTRA_CA_CERTS "$cert_crt"
|
||
else
|
||
_remove_systemd_env "$svc_file" NODE_EXTRA_CA_CERTS
|
||
fi
|
||
fi
|
||
sync_go_server_trust_proxy no
|
||
else
|
||
print_error "apply_console_protocol_mode: unknown mode '$mode'"
|
||
return 1
|
||
fi
|
||
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Enable signal/relay TLS on betterdesk-server using deployed panel cert (#219).
|
||
sync_go_server_signal_relay_tls() {
|
||
local ssl_dir="${1:-$RUSTDESK_PATH/ssl}"
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
|
||
[ -f "$go_svc_file" ] || return 0
|
||
sed -i 's/ -tls-cert [^ ]*//g' "$go_svc_file"
|
||
sed -i 's/ -tls-key [^ ]*//g' "$go_svc_file"
|
||
sed -i 's/ -tls-signal//g' "$go_svc_file"
|
||
sed -i 's/ -tls-relay//g' "$go_svc_file"
|
||
sed -i 's/ -tls-api//g' "$go_svc_file"
|
||
sed -i 's/ -force-https//g' "$go_svc_file"
|
||
sed -i "s|\(ExecStart=.*betterdesk-server[^$]*\)|\1 -tls-cert $ssl_dir/betterdesk.crt -tls-key $ssl_dir/betterdesk.key -tls-signal -tls-relay|" "$go_svc_file"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Remove signal/relay TLS from betterdesk-server (#219).
|
||
clear_go_server_signal_relay_tls() {
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
|
||
[ -f "$go_svc_file" ] || return 0
|
||
sed -i 's/ -tls-cert [^ ]*//g' "$go_svc_file"
|
||
sed -i 's/ -tls-key [^ ]*//g' "$go_svc_file"
|
||
sed -i 's/ -tls-signal//g' "$go_svc_file"
|
||
sed -i 's/ -tls-relay//g' "$go_svc_file"
|
||
sed -i 's/ -tls-api//g' "$go_svc_file"
|
||
sed -i 's/ -force-https//g' "$go_svc_file"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Enable or disable Go server reverse-proxy trust (#267 / #276).
|
||
# Optional 2nd arg: TRUSTED_PROXIES CIDR list. Omitted → loopback default.
|
||
# Empty string → enable TRUST_PROXY but do not write TRUSTED_PROXIES (remote proxy).
|
||
sync_go_server_trust_proxy() {
|
||
local enable="${1:-yes}"
|
||
local trusted_cidrs
|
||
if [ "$#" -ge 2 ]; then
|
||
trusted_cidrs="$2"
|
||
else
|
||
trusted_cidrs="127.0.0.1/32,::1/128"
|
||
fi
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
|
||
[ -f "$go_svc_file" ] || return 0
|
||
if [ "$enable" = "yes" ]; then
|
||
_upsert_systemd_env "$go_svc_file" TRUST_PROXY Y
|
||
if [ -n "$trusted_cidrs" ]; then
|
||
_upsert_systemd_env "$go_svc_file" TRUSTED_PROXIES "$trusted_cidrs"
|
||
fi
|
||
if ! grep -q '\-trust-proxy' "$go_svc_file" 2>/dev/null; then
|
||
sed -i 's|\(ExecStart=.*betterdesk-server[^$]*\)|\1 -trust-proxy|' "$go_svc_file"
|
||
fi
|
||
else
|
||
_remove_systemd_env "$go_svc_file" TRUST_PROXY
|
||
_remove_systemd_env "$go_svc_file" TRUSTED_PROXIES
|
||
sed -i 's/ -trust-proxy//g' "$go_svc_file"
|
||
fi
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Console + Go settings for TLS termination at an external reverse proxy (#267).
|
||
apply_console_reverse_proxy_mode() {
|
||
local panel_host="${1:-}"
|
||
local server_id="${2:-}"
|
||
local ws_origins="${3:-}"
|
||
local panel_bind="${4:-127.0.0.1}"
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local go_port="${GO_API_PORT:-21114}"
|
||
|
||
apply_console_protocol_mode http
|
||
clear_go_server_signal_relay_tls
|
||
|
||
_upsert_env_line "$env_file" HOST "$panel_bind"
|
||
_upsert_env_line "$env_file" TRUST_PROXY Y
|
||
# Same-host proxy: loopback. Remote proxy (HOST=0.0.0.0): operator must set the proxy CIDR.
|
||
local trusted_cidrs="127.0.0.1/32,::1/128"
|
||
if [ "$panel_bind" = "0.0.0.0" ]; then
|
||
trusted_cidrs=""
|
||
fi
|
||
if [ -n "$trusted_cidrs" ]; then
|
||
_upsert_env_line "$env_file" TRUSTED_PROXIES "$trusted_cidrs"
|
||
fi
|
||
_upsert_env_line "$env_file" HTTP_REDIRECT_HTTPS false
|
||
_upsert_env_line "$env_file" HBBS_API_URL "http://localhost:${go_port}/api"
|
||
_upsert_env_line "$env_file" BETTERDESK_API_URL "http://localhost:${go_port}/api"
|
||
|
||
if [ -n "$panel_host" ]; then
|
||
_upsert_env_line "$env_file" PANEL_PUBLIC_HOST "$panel_host"
|
||
_upsert_env_line "$env_file" PANEL_PUBLIC_URL "https://${panel_host}"
|
||
fi
|
||
if [ -n "$server_id" ]; then
|
||
_upsert_env_line "$env_file" PUBLIC_SERVER_ID "$server_id"
|
||
elif [ -n "$panel_host" ]; then
|
||
_upsert_env_line "$env_file" PUBLIC_SERVER_ID "$panel_host"
|
||
fi
|
||
if [ -n "$ws_origins" ]; then
|
||
_upsert_env_line "$env_file" WS_ALLOWED_ORIGINS "$ws_origins"
|
||
elif [ -n "$panel_host" ]; then
|
||
_upsert_env_line "$env_file" WS_ALLOWED_ORIGINS "https://${panel_host}"
|
||
fi
|
||
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" HOST "$panel_bind"
|
||
_upsert_systemd_env "$svc_file" TRUST_PROXY Y
|
||
_upsert_systemd_env "$svc_file" HTTPS_ENABLED false
|
||
_upsert_systemd_env "$svc_file" HTTP_REDIRECT_HTTPS false
|
||
_upsert_systemd_env "$svc_file" RUSTDESK_API_TLS false
|
||
fi
|
||
|
||
sync_go_server_trust_proxy yes "$trusted_cidrs"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Write Caddy/Nginx snippets and verify script under $RUSTDESK_PATH/reverse-proxy/ (#267).
|
||
generate_reverse_proxy_config() {
|
||
local panel_host="${1:-}"
|
||
local proxy_type="${2:-caddy}"
|
||
local route_wss="${3:-yes}"
|
||
local server_id="${4:-}"
|
||
local panel_bind="${5:-}"
|
||
local upstream_addr="${6:-}"
|
||
|
||
if [ -z "$panel_host" ]; then
|
||
read -p "Public panel hostname (e.g., console.example.com): " panel_host
|
||
if [ -z "$panel_host" ]; then
|
||
print_error "Hostname is required for reverse-proxy snippets"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$panel_bind" ]; then
|
||
if confirm "Is the reverse proxy on THIS server (same host as BetterDesk)?"; then
|
||
panel_bind="127.0.0.1"
|
||
upstream_addr="127.0.0.1"
|
||
else
|
||
panel_bind="0.0.0.0"
|
||
upstream_addr=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
||
[ -z "$upstream_addr" ] && upstream_addr=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
echo ""
|
||
read -p "BetterDesk LAN IP for proxy upstream [${upstream_addr}]: " _custom_up
|
||
[ -n "$_custom_up" ] && upstream_addr="$_custom_up"
|
||
if [ -z "$upstream_addr" ]; then
|
||
print_error "LAN IP required when the proxy runs on another host"
|
||
return 1
|
||
fi
|
||
print_warning "Panel will listen on 0.0.0.0:5000 — restrict firewall to your proxy host"
|
||
fi
|
||
fi
|
||
[ -z "$upstream_addr" ] && upstream_addr="$panel_bind"
|
||
if [ "$panel_bind" = "0.0.0.0" ] && [ "$upstream_addr" = "0.0.0.0" ]; then
|
||
upstream_addr=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
||
fi
|
||
|
||
if [ -z "$proxy_type" ] || [ "$proxy_type" = "prompt" ]; then
|
||
echo ""
|
||
echo " 1) Caddy"
|
||
echo " 2) Nginx"
|
||
read -p "Proxy type [1]: " _proxy_pick
|
||
case "${_proxy_pick:-1}" in
|
||
2) proxy_type="nginx" ;;
|
||
*) proxy_type="caddy" ;;
|
||
esac
|
||
fi
|
||
|
||
if [ -z "$route_wss" ] || [ "$route_wss" = "prompt" ]; then
|
||
if confirm "Route RustDesk WSS paths (/ws/id, /ws/relay) on the same hostname?"; then
|
||
route_wss="yes"
|
||
else
|
||
route_wss="no"
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$server_id" ]; then
|
||
if confirm "Use a different hostname for RustDesk ID/relay clients than the panel?"; then
|
||
read -p "RustDesk ID server hostname (e.g., desk.example.com): " server_id
|
||
fi
|
||
fi
|
||
[ -z "$server_id" ] && server_id="$panel_host"
|
||
|
||
local out_dir="$RUSTDESK_PATH/reverse-proxy"
|
||
mkdir -p "$out_dir"
|
||
|
||
local ws_origins="https://${panel_host}"
|
||
[ "$panel_host" != "$server_id" ] && ws_origins="${ws_origins},https://${server_id}"
|
||
|
||
cat > "$out_dir/betterdesk.env.snippet" << EOF
|
||
# BetterDesk reverse-proxy mode (#267) — merge into $CONSOLE_PATH/.env
|
||
HOST=${panel_bind}
|
||
HTTPS_ENABLED=false
|
||
HTTP_REDIRECT_HTTPS=false
|
||
TRUST_PROXY=Y
|
||
TRUSTED_PROXIES=127.0.0.1/32,::1/128
|
||
PORT=5000
|
||
PANEL_PUBLIC_HOST=${panel_host}
|
||
PANEL_PUBLIC_URL=https://${panel_host}
|
||
PUBLIC_SERVER_ID=${server_id}
|
||
WS_ALLOWED_ORIGINS=${ws_origins}
|
||
EOF
|
||
|
||
if [ "$proxy_type" = "nginx" ]; then
|
||
cat > "$out_dir/nginx.betterdesk.conf.snippet" << EOF
|
||
# BetterDesk reverse-proxy snippet (#267) — merge into your nginx site config.
|
||
# TLS certificates: use certbot --nginx or your existing cert setup.
|
||
|
||
map \$http_upgrade \$connection_upgrade {
|
||
default upgrade;
|
||
'' close;
|
||
}
|
||
|
||
server {
|
||
listen 80;
|
||
server_name ${panel_host};
|
||
|
||
client_max_body_size 100M;
|
||
EOF
|
||
if [ "$route_wss" = "yes" ]; then
|
||
cat >> "$out_dir/nginx.betterdesk.conf.snippet" << EOF
|
||
|
||
location = /ws/id {
|
||
proxy_pass http://${upstream_addr}:21118;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection "Upgrade";
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_buffering off;
|
||
proxy_read_timeout 120s;
|
||
proxy_send_timeout 120s;
|
||
}
|
||
|
||
location = /ws/relay {
|
||
proxy_pass http://${upstream_addr}:21119;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection "Upgrade";
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_buffering off;
|
||
proxy_read_timeout 120s;
|
||
proxy_send_timeout 120s;
|
||
}
|
||
EOF
|
||
fi
|
||
cat >> "$out_dir/nginx.betterdesk.conf.snippet" << EOF
|
||
|
||
location ~ ^/ws/ {
|
||
proxy_pass http://${upstream_addr}:5000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection \$connection_upgrade;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_buffering off;
|
||
proxy_cache off;
|
||
proxy_read_timeout 86400s;
|
||
proxy_send_timeout 86400s;
|
||
proxy_socket_keepalive on;
|
||
}
|
||
|
||
location / {
|
||
proxy_pass http://${upstream_addr}:5000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection \$connection_upgrade;
|
||
proxy_read_timeout 86400s;
|
||
}
|
||
}
|
||
EOF
|
||
print_success "Nginx snippet: $out_dir/nginx.betterdesk.conf.snippet"
|
||
else
|
||
cat > "$out_dir/caddy.Caddyfile.snippet" << EOF
|
||
# BetterDesk reverse-proxy snippet (#267) — merge into /etc/caddy/Caddyfile
|
||
# Caddy obtains TLS automatically when this block is active.
|
||
|
||
${panel_host} {
|
||
EOF
|
||
if [ "$route_wss" = "yes" ]; then
|
||
cat >> "$out_dir/caddy.Caddyfile.snippet" << EOF
|
||
handle /ws/id {
|
||
reverse_proxy ${upstream_addr}:21118
|
||
}
|
||
handle /ws/relay {
|
||
reverse_proxy ${upstream_addr}:21119
|
||
}
|
||
EOF
|
||
fi
|
||
cat >> "$out_dir/caddy.Caddyfile.snippet" << EOF
|
||
reverse_proxy ${upstream_addr}:5000
|
||
|
||
encode gzip zstd
|
||
header {
|
||
X-Content-Type-Options nosniff
|
||
X-Frame-Options DENY
|
||
Referrer-Policy strict-origin-when-cross-origin
|
||
}
|
||
}
|
||
EOF
|
||
if [ "$panel_host" != "$server_id" ]; then
|
||
cat >> "$out_dir/caddy.Caddyfile.snippet" << EOF
|
||
|
||
# Optional second site when ID/relay clients use a different hostname:
|
||
# ${server_id} {
|
||
# handle /ws/id { reverse_proxy ${upstream_addr}:21118 }
|
||
# handle /ws/relay { reverse_proxy ${upstream_addr}:21119 }
|
||
# }
|
||
EOF
|
||
fi
|
||
print_success "Caddy snippet: $out_dir/caddy.Caddyfile.snippet"
|
||
fi
|
||
|
||
cat > "$out_dir/firewall-notes.txt" << EOF
|
||
BetterDesk reverse-proxy firewall (#267)
|
||
|
||
Through your reverse proxy (HTTPS :443):
|
||
- Panel + console WebSockets -> http://${upstream_addr}:5000
|
||
$( [ "$route_wss" = "yes" ] && echo " - RustDesk WSS /ws/id -> ${upstream_addr}:21118, /ws/relay -> ${upstream_addr}:21119" )
|
||
$( [ "$panel_bind" = "0.0.0.0" ] && echo " - Panel bind: 0.0.0.0:5000 (remote proxy) — restrict :5000 to proxy IP in firewall" )
|
||
|
||
Must reach this host directly (not HTTP reverse-proxied):
|
||
- 21116/tcp + 21116/udp Signal
|
||
- 21117/tcp Relay
|
||
- 21121/tcp Client API (unless proxied separately)
|
||
|
||
Example (ufw):
|
||
sudo ufw allow 443/tcp
|
||
sudo ufw allow 21116/tcp
|
||
sudo ufw allow 21116/udp
|
||
sudo ufw allow 21117/tcp
|
||
sudo ufw allow 21121/tcp
|
||
EOF
|
||
|
||
cat > "$out_dir/verify.sh" << 'VERIFYEOF'
|
||
#!/usr/bin/env bash
|
||
# BetterDesk reverse-proxy verification (#267)
|
||
set -euo pipefail
|
||
PANEL_HOST="${1:-}"
|
||
if [ -z "$PANEL_HOST" ]; then
|
||
echo "Usage: $0 <public-hostname>"
|
||
exit 1
|
||
fi
|
||
echo "=== Local panel (HTTP) ==="
|
||
curl -sI "http://127.0.0.1:5000/" | head -5 || true
|
||
echo ""
|
||
echo "=== Public panel (HTTPS via proxy) ==="
|
||
curl -sI "https://${PANEL_HOST}/" | head -5 || true
|
||
echo ""
|
||
echo "=== Console WebSocket upgrade ==="
|
||
curl -i -N --max-time 8 \
|
||
-H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||
-H "Sec-WebSocket-Version: 13" \
|
||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||
"https://${PANEL_HOST}/ws/bd-signal" 2>/dev/null | head -8 || true
|
||
VERIFYEOF
|
||
chmod +x "$out_dir/verify.sh"
|
||
# Inject hostname into verify script usage (already passed as arg)
|
||
|
||
echo ""
|
||
print_info "Reverse-proxy files written to: $out_dir"
|
||
print_info " betterdesk.env.snippet"
|
||
[ "$proxy_type" = "nginx" ] && print_info " nginx.betterdesk.conf.snippet" || print_info " caddy.Caddyfile.snippet"
|
||
print_info " verify.sh $panel_host"
|
||
print_info " firewall-notes.txt"
|
||
print_info "Documentation: docs/setup/REVERSE_PROXY.md"
|
||
echo ""
|
||
print_warning "Configure your proxy, then open https://${panel_host}/ (not :5443)"
|
||
if [ "$panel_host" != "$server_id" ]; then
|
||
print_info "RustDesk clients: ID server ${server_id} (set PUBLIC_SERVER_ID in .env)"
|
||
fi
|
||
|
||
REVERSE_PROXY_GENERATED_HOST="$panel_host"
|
||
REVERSE_PROXY_GENERATED_SERVER_ID="$server_id"
|
||
REVERSE_PROXY_GENERATED_WS_ORIGINS="$ws_origins"
|
||
REVERSE_PROXY_PANEL_BIND="$panel_bind"
|
||
REVERSE_PROXY_UPSTREAM_ADDR="$upstream_addr"
|
||
}
|
||
|
||
# Interactive reverse-proxy wizard: apply BetterDesk settings + emit proxy snippets (#267).
|
||
do_configure_reverse_proxy() {
|
||
local panel_host server_id ws_origins panel_bind
|
||
|
||
echo ""
|
||
print_step "Configuring BetterDesk for external reverse proxy (TLS at Caddy/Nginx)..."
|
||
print_info "Your proxy terminates TLS on :443; BetterDesk panel stays plain HTTP"
|
||
echo ""
|
||
|
||
read -p "Public panel hostname (e.g., console.example.com): " panel_host
|
||
if [ -z "$panel_host" ]; then
|
||
print_error "Hostname is required"
|
||
return 1
|
||
fi
|
||
|
||
if ! generate_reverse_proxy_config "$panel_host" "prompt" "prompt" ""; then
|
||
return 1
|
||
fi
|
||
|
||
server_id="${REVERSE_PROXY_GENERATED_SERVER_ID:-$panel_host}"
|
||
ws_origins="${REVERSE_PROXY_GENERATED_WS_ORIGINS:-https://${panel_host}}"
|
||
panel_bind="${REVERSE_PROXY_PANEL_BIND:-127.0.0.1}"
|
||
|
||
apply_console_reverse_proxy_mode "$panel_host" "$server_id" "$ws_origins" "$panel_bind"
|
||
|
||
print_success "BetterDesk configured for external reverse proxy"
|
||
echo ""
|
||
if [ "$panel_bind" = "0.0.0.0" ]; then
|
||
print_info " Panel (bind): http://0.0.0.0:$(resolve_panel_http_port) (remote proxy host)"
|
||
print_info " Proxy upstream: http://${REVERSE_PROXY_UPSTREAM_ADDR:-<lan-ip>}:$(resolve_panel_http_port)"
|
||
else
|
||
print_info " Panel (local): http://127.0.0.1:$(resolve_panel_http_port)"
|
||
fi
|
||
print_info " Panel (public): https://${panel_host}/"
|
||
print_info " TRUST_PROXY: Y (console + Go server)"
|
||
if [ "$panel_bind" = "0.0.0.0" ]; then
|
||
print_info " TRUSTED_PROXIES: set to your reverse-proxy IP/CIDR in .env (required for Go WSS)"
|
||
else
|
||
print_info " TRUSTED_PROXIES: 127.0.0.1/32,::1/128 (same-host)"
|
||
fi
|
||
print_info " Signal/Relay: TCP :21116 / :21117 (direct — not HTTP-proxied)"
|
||
echo ""
|
||
print_info "Copy proxy snippet from $RUSTDESK_PATH/reverse-proxy/ into Caddy/Nginx, then reload the proxy."
|
||
}
|
||
|
||
# HTTP redirect listener port (always PORT, default 5000).
|
||
resolve_panel_http_port() {
|
||
read_effective_console_setting PORT 5000
|
||
}
|
||
|
||
# HTTPS panel listen port (HTTPS_PORT, default 5443 — never conflated with PORT).
|
||
resolve_panel_https_port() {
|
||
read_effective_console_setting HTTPS_PORT 5443
|
||
}
|
||
|
||
# Primary panel port for health checks: HTTPS_PORT when HTTPS enabled, else PORT.
|
||
resolve_panel_health_port() {
|
||
local https_enabled
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
resolve_panel_https_port
|
||
else
|
||
resolve_panel_http_port
|
||
fi
|
||
}
|
||
|
||
# Offer native HTTPS on standard port 443 after enabling TLS (#219 follow-up).
|
||
maybe_offer_standard_https_port() {
|
||
local env_file="${CONSOLE_PATH}/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local current_https_port
|
||
|
||
current_https_port=$(read_effective_console_setting HTTPS_PORT 5443)
|
||
[ "$current_https_port" = "443" ] && return 0
|
||
|
||
echo ""
|
||
print_info "Panel HTTPS defaults to port 5443 (avoids conflicts with nginx/certbot on :443)."
|
||
if confirm "Use standard HTTPS port 443 (https://your-domain without :5443)?"; then
|
||
_upsert_env_line "$env_file" HTTPS_PORT 443
|
||
_upsert_env_line "$env_file" PORT 80
|
||
_upsert_env_line "$env_file" HTTP_REDIRECT_HTTPS true
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" HTTPS_PORT 443
|
||
_upsert_systemd_env "$svc_file" PORT 80
|
||
_upsert_systemd_env "$svc_file" HTTP_REDIRECT_HTTPS true
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
fi
|
||
ensure_betterdesk_console_user >/dev/null
|
||
print_success "Standard ports configured: HTTPS :443, HTTP redirect :80"
|
||
print_info "Ensure nothing else listens on :443/:80; open firewall: ufw allow 443/tcp (and 80/tcp if redirecting)"
|
||
fi
|
||
}
|
||
|
||
# True when Client API (:21121) should be probed over HTTPS (RUSTDESK_API_TLS + cert present).
|
||
client_api_should_use_tls() {
|
||
local mode cert_path key_path
|
||
mode=$(read_effective_console_setting RUSTDESK_API_TLS auto)
|
||
mode=$(echo "$mode" | tr '[:upper:]' '[:lower:]')
|
||
if [ "$mode" = "false" ] || [ "$mode" = "0" ] || [ "$mode" = "off" ] || [ "$mode" = "http" ]; then
|
||
return 1
|
||
fi
|
||
cert_path=$(read_effective_console_setting SSL_CERT_PATH "")
|
||
key_path=$(read_effective_console_setting SSL_KEY_PATH "")
|
||
if [ -z "$cert_path" ] || [ ! -f "$cert_path" ] || [ -z "$key_path" ] || [ ! -f "$key_path" ]; then
|
||
return 1
|
||
fi
|
||
if [ "$mode" = "true" ] || [ "$mode" = "1" ] || [ "$mode" = "on" ] || [ "$mode" = "https" ]; then
|
||
return 0
|
||
fi
|
||
# auto — certs present
|
||
return 0
|
||
}
|
||
|
||
# True when an existing panel auth store is present (update must not show credential banner).
|
||
has_existing_panel_auth() {
|
||
if [ -f "$CONSOLE_PATH/data/auth.db" ]; then
|
||
return 0
|
||
fi
|
||
if [ "${USE_POSTGRESQL:-false}" = "true" ]; then
|
||
return 0
|
||
fi
|
||
if [ -f "$CONSOLE_PATH/.env" ] && grep -q '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# On update: reset failed state and sync console service user permissions before start.
|
||
prepare_console_after_update() {
|
||
if [ ! -f "$CONSOLE_PATH/server.js" ]; then
|
||
return 0
|
||
fi
|
||
systemctl reset-failed betterdesk-console 2>/dev/null || true
|
||
repair_console_service_user_line "betterdesk" || true
|
||
repair_https_stuck_state yes || true
|
||
if [ -f "$CONSOLE_PATH/scripts/linux-ensure-console-user.js" ] && command -v node &>/dev/null; then
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
node "$CONSOLE_PATH/scripts/linux-ensure-console-user.js" || print_warning "Console permission sync reported issues"
|
||
else
|
||
print_warning "Console permission sync skipped; run linux-ensure-console-user.js as root"
|
||
fi
|
||
fi
|
||
repair_console_service_user_line "betterdesk" || true
|
||
ensure_console_tls_material_readable 2>/dev/null || true
|
||
return 0
|
||
}
|
||
|
||
maybe_create_admin_user_on_update() {
|
||
if has_existing_panel_auth; then
|
||
print_info "Existing installation — admin accounts preserved"
|
||
return 0
|
||
fi
|
||
create_admin_user
|
||
}
|
||
|
||
# Start / restart betterdesk-console and verify panel health (#306).
|
||
# Always attempts start even when earlier helper steps failed (set -e safe).
|
||
start_betterdesk_console_verified() {
|
||
local panel_port console_state
|
||
panel_port=$(resolve_panel_health_port)
|
||
|
||
print_info "Starting betterdesk-console (Node.js)..."
|
||
systemctl reset-failed betterdesk-console 2>/dev/null || true
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null; then
|
||
systemctl restart betterdesk-console || true
|
||
else
|
||
# Prefer start when inactive (post graceful_stop); fall back to restart.
|
||
systemctl start betterdesk-console 2>/dev/null || systemctl restart betterdesk-console || true
|
||
fi
|
||
sleep 2
|
||
|
||
if ! verify_service_health "betterdesk-console" "$panel_port" 10; then
|
||
print_warning "Web console may not be running correctly"
|
||
console_state=$(systemctl show betterdesk-console --property=ActiveState --value 2>/dev/null || echo "unknown")
|
||
print_error "betterdesk-console ActiveState=${console_state} (expected: active)"
|
||
print_info " Possible causes: npm modules, TLS key permissions, port ${panel_port} conflict"
|
||
print_info "Run: journalctl -u betterdesk-console -n 50 --no-pager"
|
||
print_info "Then: sudo systemctl start betterdesk-console"
|
||
return 1
|
||
fi
|
||
|
||
print_success "betterdesk-console started and healthy (port ${panel_port})"
|
||
return 0
|
||
}
|
||
|
||
# Start services with health verification
|
||
start_services_with_verification() {
|
||
print_step "Starting services with health verification..."
|
||
|
||
local has_errors=false
|
||
local console_ok=true
|
||
|
||
# Check ports before starting
|
||
if ! check_port_available "21116" "signal"; then
|
||
print_error "Port 21116 (ID server) is not available"
|
||
has_errors=true
|
||
fi
|
||
|
||
if ! check_port_available "21117" "relay"; then
|
||
print_error "Port 21117 (relay) is not available"
|
||
has_errors=true
|
||
fi
|
||
|
||
if [ "$has_errors" = true ]; then
|
||
print_error "Cannot start services - ports are in use"
|
||
print_info "Try: sudo lsof -i :21116 and sudo lsof -i :21117 to find conflicts"
|
||
return 1
|
||
fi
|
||
|
||
# Enable services
|
||
systemctl enable betterdesk-server betterdesk-console 2>/dev/null || true
|
||
|
||
# Start Go server (signal + relay + API in one binary)
|
||
print_info "Starting betterdesk-server (Go)..."
|
||
systemctl start betterdesk-server
|
||
sleep 3
|
||
|
||
if ! verify_service_health "betterdesk-server" "21116" 10; then
|
||
print_error "Failed to start betterdesk-server"
|
||
print_info "Service state: $(systemctl show betterdesk-server --property=ActiveState --value 2>/dev/null)"
|
||
print_info "Run: journalctl -u betterdesk-server -n 50 --no-pager"
|
||
# Still try to bring console up — operator may recover Go separately (#306)
|
||
prepare_console_after_update || true
|
||
start_betterdesk_console_verified || true
|
||
return 1
|
||
fi
|
||
print_success "betterdesk-server started and healthy"
|
||
|
||
# Inject shared API key into Go server database for Node.js ↔ Go communication.
|
||
# Must not abort under set -e (sqlite3 busy/locked after Go start was leaving Console down — #306).
|
||
local api_key_file="$RUSTDESK_PATH/.api_key"
|
||
if [ -f "$api_key_file" ]; then
|
||
local api_key
|
||
api_key=$(cat "$api_key_file" 2>/dev/null || true)
|
||
local api_key_sql=""
|
||
if [ -n "$api_key" ]; then
|
||
api_key_sql=$(sql_escape_literal "$api_key")
|
||
fi
|
||
local go_db="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
if [ -n "$api_key_sql" ] && [ -f "$go_db" ] && command -v sqlite3 &>/dev/null; then
|
||
if sqlite3 "$go_db" "INSERT OR REPLACE INTO server_config (key, value) VALUES ('api_key', '$api_key_sql');" 2>/dev/null; then
|
||
print_info "API key synced to Go server database"
|
||
else
|
||
print_warning "API key sync to Go DB skipped (sqlite3 failed — Console start continues)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Verify relay port is also listening
|
||
if ! verify_service_health "betterdesk-server" "21117" 5; then
|
||
print_warning "Relay port 21117 may not be ready yet"
|
||
fi
|
||
|
||
# Re-sync permissions after Go server may have created root-owned DB/WAL files (#206)
|
||
# Never abort start path under set -e (#306)
|
||
prepare_console_after_update || print_warning "Console prep after update reported issues (continuing)"
|
||
|
||
if ! start_betterdesk_console_verified; then
|
||
console_ok=false
|
||
fi
|
||
|
||
if [ "$console_ok" = true ]; then
|
||
print_success "All services started and verified"
|
||
return 0
|
||
fi
|
||
|
||
print_error "Server is running but web console failed to start"
|
||
print_info "Run: journalctl -u betterdesk-console -n 50 --no-pager"
|
||
print_info "Then: sudo systemctl start betterdesk-console"
|
||
return 1
|
||
}
|
||
|
||
#===============================================================================
|
||
# Detection Functions
|
||
#===============================================================================
|
||
|
||
detect_installation() {
|
||
INSTALL_STATUS="none"
|
||
HBBS_RUNNING=false
|
||
HBBR_RUNNING=false
|
||
CONSOLE_RUNNING=false
|
||
BINARIES_OK=false
|
||
DATABASE_OK=false
|
||
CONSOLE_TYPE="none"
|
||
|
||
# Check paths
|
||
if [ -d "$RUSTDESK_PATH" ]; then
|
||
INSTALL_STATUS="partial"
|
||
|
||
# Check Go server binary (primary) or legacy Rust binaries
|
||
if [ -f "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
BINARIES_OK=true
|
||
SERVER_TYPE="go"
|
||
elif [ -f "$RUSTDESK_PATH/hbbs" ] || [ -f "$RUSTDESK_PATH/hbbs-v8-api" ]; then
|
||
BINARIES_OK=true
|
||
SERVER_TYPE="rust"
|
||
print_warning "Legacy Rust binaries detected. Consider upgrading to Go server."
|
||
fi
|
||
|
||
# Check database (SQLite file or PostgreSQL connection)
|
||
local detected_db_type="sqlite"
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
detected_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
detected_db_type="${detected_db_type:-sqlite}"
|
||
fi
|
||
|
||
if [ "$detected_db_type" = "postgres" ]; then
|
||
# PostgreSQL: check via systemd service config or .env
|
||
local pg_uri
|
||
pg_uri=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
if [ -n "$pg_uri" ]; then
|
||
if PGCONNECT_TIMEOUT=3 psql "$pg_uri" -c "SELECT 1" &>/dev/null 2>&1; then
|
||
DATABASE_OK=true
|
||
fi
|
||
fi
|
||
else
|
||
# SQLite: check file exists
|
||
if [ -f "$DB_PATH" ]; then
|
||
DATABASE_OK=true
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Detect console type
|
||
if [ -d "$CONSOLE_PATH" ]; then
|
||
if [ -f "$CONSOLE_PATH/server.js" ] || [ -f "$CONSOLE_PATH/package.json" ]; then
|
||
CONSOLE_TYPE="nodejs"
|
||
elif [ -f "$CONSOLE_PATH/app.py" ]; then
|
||
CONSOLE_TYPE="nodejs" # Flask detected, will be migrated to Node.js
|
||
print_warning "Legacy Flask console detected. It will be migrated to Node.js on update."
|
||
fi
|
||
|
||
if [ "$CONSOLE_TYPE" != "none" ] && [ "$BINARIES_OK" = true ] && [ "$DATABASE_OK" = true ]; then
|
||
INSTALL_STATUS="complete"
|
||
fi
|
||
fi
|
||
|
||
# Check services (Go server or legacy Rust)
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
HBBS_RUNNING=true
|
||
HBBR_RUNNING=true # Go server handles both
|
||
elif systemctl is-active --quiet rustdesksignal 2>/dev/null || \
|
||
systemctl is-active --quiet hbbs 2>/dev/null; then
|
||
HBBS_RUNNING=true
|
||
fi
|
||
|
||
if ! [ "$HBBR_RUNNING" = true ]; then
|
||
if systemctl is-active --quiet rustdeskrelay 2>/dev/null || \
|
||
systemctl is-active --quiet hbbr 2>/dev/null; then
|
||
HBBR_RUNNING=true
|
||
fi
|
||
fi
|
||
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null || \
|
||
systemctl is-active --quiet betterdesk 2>/dev/null; then
|
||
CONSOLE_RUNNING=true
|
||
fi
|
||
}
|
||
|
||
# Preserve database configuration from existing .env file
|
||
# This MUST be called before install_nodejs_console() during UPDATE/REPAIR
|
||
# to prevent switching from PostgreSQL to SQLite
|
||
preserve_database_config() {
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
local existing_db_type existing_db_url
|
||
existing_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
existing_db_url=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
|
||
if [ "$existing_db_type" = "postgres" ] && [ -n "$existing_db_url" ]; then
|
||
USE_POSTGRESQL="true"
|
||
POSTGRESQL_URI="$existing_db_url"
|
||
print_info "Preserving PostgreSQL configuration from existing .env"
|
||
elif [ "$existing_db_type" = "sqlite" ]; then
|
||
USE_POSTGRESQL="false"
|
||
POSTGRESQL_URI=""
|
||
print_info "Preserving SQLite configuration from existing .env"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Write or merge console .env from web-nodejs/.env.example (issue #158).
|
||
# Usage: merge_console_env true — fresh install (full template)
|
||
# merge_console_env false — update (append missing keys only)
|
||
merge_console_env() {
|
||
local fresh_install="${1:-false}"
|
||
local merge_script=""
|
||
local subst_script=""
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local db_type="sqlite"
|
||
local database_url=""
|
||
local admin_password="${ADMIN_PASSWORD:-}"
|
||
local session_secret=""
|
||
local subst_file="/tmp/betterdesk-env-subst-$$.json"
|
||
local go_port="${GO_API_PORT:-21114}"
|
||
local client_port="${CLIENT_API_PORT:-21121}"
|
||
|
||
if [ -f "$CONSOLE_PATH/scripts/merge-env.js" ]; then
|
||
merge_script="$CONSOLE_PATH/scripts/merge-env.js"
|
||
subst_script="$CONSOLE_PATH/scripts/write-installer-env-subst.js"
|
||
elif [ -f "$SCRIPT_DIR/web-nodejs/scripts/merge-env.js" ]; then
|
||
merge_script="$SCRIPT_DIR/web-nodejs/scripts/merge-env.js"
|
||
subst_script="$SCRIPT_DIR/web-nodejs/scripts/write-installer-env-subst.js"
|
||
else
|
||
print_error "merge-env.js not found — cannot configure .env"
|
||
return 1
|
||
fi
|
||
|
||
if [ "$USE_POSTGRESQL" = "true" ] && [ -n "$POSTGRESQL_URI" ]; then
|
||
db_type="postgres"
|
||
database_url="$POSTGRESQL_URI"
|
||
fi
|
||
|
||
if [ "$fresh_install" = "true" ]; then
|
||
if [ -z "$admin_password" ]; then
|
||
admin_password=$(openssl rand -hex 16)
|
||
fi
|
||
session_secret=$(openssl rand -hex 32)
|
||
else
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
session_secret=$(grep -m1 '^SESSION_SECRET=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
if [ -z "$admin_password" ]; then
|
||
admin_password=$(grep -m1 '^DEFAULT_ADMIN_PASSWORD=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
fi
|
||
fi
|
||
if [ -z "$session_secret" ]; then
|
||
session_secret=$(openssl rand -hex 32)
|
||
fi
|
||
fi
|
||
|
||
local fresh_flag=""
|
||
[ "$fresh_install" = "true" ] && fresh_flag="--fresh"
|
||
|
||
export BD_SUBST_RUSTDESK_DIR="$RUSTDESK_PATH"
|
||
export BD_SUBST_PUB_KEY_PATH="$RUSTDESK_PATH/id_ed25519.pub"
|
||
export BD_SUBST_API_KEY_PATH="$RUSTDESK_PATH/.api_key"
|
||
export BD_SUBST_DB_TYPE="$db_type"
|
||
export BD_SUBST_DB_PATH="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
export BD_SUBST_DATABASE_URL="$database_url"
|
||
export BD_SUBST_DATA_DIR="$CONSOLE_PATH/data"
|
||
export BD_SUBST_GO_API_PORT="$go_port"
|
||
export BD_SUBST_HBBS_API_URL="http://localhost:${go_port}/api"
|
||
export BD_SUBST_BETTERDESK_API_URL="http://localhost:${go_port}/api"
|
||
export BD_SUBST_API_PORT="$client_port"
|
||
export BD_SUBST_DEFAULT_ADMIN_PASSWORD="$admin_password"
|
||
export BD_SUBST_SESSION_SECRET="$session_secret"
|
||
export BD_SUBST_SSL_CERT_PATH="$ssl_dir/betterdesk.crt"
|
||
export BD_SUBST_SSL_KEY_PATH="$ssl_dir/betterdesk.key"
|
||
|
||
if [ -f "$subst_script" ]; then
|
||
node "$subst_script" "$subst_file" 2>/dev/null || true
|
||
fi
|
||
|
||
local merge_ok=false
|
||
local merge_output=""
|
||
local had_subst_file=false
|
||
if [ -f "$subst_file" ]; then
|
||
had_subst_file=true
|
||
merge_output=$(node "$merge_script" --target "$CONSOLE_PATH/.env" $fresh_flag --subst-file "$subst_file" 2>&1) || true
|
||
if echo "$merge_output" | grep -q '"success":true'; then
|
||
merge_ok=true
|
||
fi
|
||
rm -f "$subst_file"
|
||
fi
|
||
|
||
if [ "$merge_ok" != true ]; then
|
||
print_error "Failed to write .env via merge-env.js"
|
||
if [ -n "$merge_output" ]; then
|
||
print_info "$merge_output"
|
||
elif [ "$had_subst_file" != true ]; then
|
||
print_info "Substitution file was not created (check write-installer-env-subst.js)"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
chmod 600 "$CONSOLE_PATH/.env" 2>/dev/null || true
|
||
if [ "$fresh_install" = "true" ]; then
|
||
print_info "Created .env configuration file (fresh install)"
|
||
else
|
||
print_info "Merged new .env keys (existing settings preserved)"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
detect_architecture() {
|
||
ARCH=$(uname -m)
|
||
case "$ARCH" in
|
||
x86_64) ARCH_NAME="x86_64" ;;
|
||
aarch64|arm64) ARCH_NAME="aarch64" ;;
|
||
armv7l) ARCH_NAME="armv7" ;;
|
||
*) ARCH_NAME="unknown" ;;
|
||
esac
|
||
}
|
||
|
||
detect_os() {
|
||
if [ -f /etc/os-release ]; then
|
||
OS_NAME=$(grep -m1 '^NAME=' /etc/os-release | cut -d= -f2- | sed 's/^"//; s/"$//' || echo "Unknown")
|
||
OS_VERSION=$(grep -m1 '^VERSION_ID=' /etc/os-release | cut -d= -f2- | sed 's/^"//; s/"$//' || echo "")
|
||
else
|
||
OS_NAME="Unknown"
|
||
OS_VERSION=""
|
||
fi
|
||
}
|
||
|
||
# Auto-detect RustDesk installation path
|
||
auto_detect_paths() {
|
||
local found=false
|
||
|
||
# If RUSTDESK_PATH is already set (via env var), validate it
|
||
if [ -n "$RUSTDESK_PATH" ]; then
|
||
if [ -d "$RUSTDESK_PATH" ] && { [ -f "$RUSTDESK_PATH/betterdesk-server" ] || [ -f "$RUSTDESK_PATH/hbbs" ] || [ -f "$RUSTDESK_PATH/hbbs-v8-api" ]; }; then
|
||
print_info "Using configured RustDesk path: $RUSTDESK_PATH"
|
||
found=true
|
||
else
|
||
print_warning "Configured RUSTDESK_PATH ($RUSTDESK_PATH) is invalid"
|
||
RUSTDESK_PATH=""
|
||
fi
|
||
fi
|
||
|
||
# Auto-detect if not found
|
||
if [ -z "$RUSTDESK_PATH" ]; then
|
||
for path in "${COMMON_RUSTDESK_PATHS[@]}"; do
|
||
if [ -d "$path" ] && { [ -f "$path/betterdesk-server" ] || [ -f "$path/hbbs" ] || [ -f "$path/hbbs-v8-api" ]; }; then
|
||
RUSTDESK_PATH="$path"
|
||
print_success "Detected RustDesk installation: $RUSTDESK_PATH"
|
||
found=true
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# If still not found, use default for new installations
|
||
if [ -z "$RUSTDESK_PATH" ]; then
|
||
RUSTDESK_PATH="/opt/betterdesk"
|
||
print_info "No installation detected. Default path: $RUSTDESK_PATH"
|
||
fi
|
||
|
||
# Auto-detect Console path and type
|
||
CONSOLE_TYPE="none"
|
||
|
||
if [ -n "$CONSOLE_PATH" ]; then
|
||
# Check for Node.js console first
|
||
if [ -d "$CONSOLE_PATH" ] && { [ -f "$CONSOLE_PATH/server.js" ] || [ -f "$CONSOLE_PATH/package.json" ]; }; then
|
||
CONSOLE_TYPE="nodejs"
|
||
print_info "Using configured Node.js Console path: $CONSOLE_PATH"
|
||
elif [ -d "$CONSOLE_PATH" ] && [ -f "$CONSOLE_PATH/app.py" ]; then
|
||
CONSOLE_TYPE="nodejs" # Legacy Flask, will be migrated
|
||
print_warning "Legacy Flask console detected at $CONSOLE_PATH — will be migrated to Node.js"
|
||
else
|
||
print_warning "Configured CONSOLE_PATH ($CONSOLE_PATH) is invalid"
|
||
CONSOLE_PATH=""
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$CONSOLE_PATH" ]; then
|
||
for path in "${COMMON_CONSOLE_PATHS[@]}"; do
|
||
# Check for Node.js console first
|
||
if [ -d "$path" ] && { [ -f "$path/server.js" ] || [ -f "$path/package.json" ]; }; then
|
||
CONSOLE_PATH="$path"
|
||
CONSOLE_TYPE="nodejs"
|
||
print_success "Detected Node.js Console: $CONSOLE_PATH"
|
||
break
|
||
fi
|
||
# Check for legacy Flask console (will be migrated)
|
||
if [ -d "$path" ] && [ -f "$path/app.py" ]; then
|
||
CONSOLE_PATH="$path"
|
||
CONSOLE_TYPE="nodejs"
|
||
print_warning "Legacy Flask console detected at $CONSOLE_PATH — will be migrated to Node.js"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Default Console path if not found
|
||
if [ -z "$CONSOLE_PATH" ]; then
|
||
CONSOLE_PATH="/opt/BetterDeskConsole"
|
||
fi
|
||
|
||
# Update DB_PATH based on detected RUSTDESK_PATH
|
||
DB_PATH="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
|
||
return 0
|
||
}
|
||
|
||
# Interactive path configuration
|
||
configure_paths() {
|
||
local _menu_items=(
|
||
$'Auto-detect paths\tScan common install locations'
|
||
$'Set server path\tManually set the RustDesk server path'
|
||
$'Set console path\tManually set the web console path'
|
||
$'Reset to defaults\t/opt/betterdesk + /opt/BetterDeskConsole'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 0 )
|
||
local subtitle="server: ${RUSTDESK_PATH:-unset} | console: ${CONSOLE_PATH:-unset}"
|
||
menu_choose "Path Configuration" "$subtitle"
|
||
local choice="$MENU_CHOICE"
|
||
|
||
case $choice in
|
||
1)
|
||
RUSTDESK_PATH=""
|
||
CONSOLE_PATH=""
|
||
auto_detect_paths
|
||
press_enter
|
||
configure_paths
|
||
;;
|
||
2)
|
||
echo ""
|
||
echo -n "Enter RustDesk server path (e.g., /opt/rustdesk): "
|
||
read -r new_path
|
||
if [ -n "$new_path" ]; then
|
||
if [ -d "$new_path" ]; then
|
||
RUSTDESK_PATH="$new_path"
|
||
DB_PATH="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
print_success "RustDesk path set to: $RUSTDESK_PATH"
|
||
else
|
||
print_warning "Directory does not exist: $new_path"
|
||
if confirm "Create this directory?"; then
|
||
mkdir -p "$new_path"
|
||
RUSTDESK_PATH="$new_path"
|
||
DB_PATH="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
print_success "Created and set RustDesk path: $RUSTDESK_PATH"
|
||
fi
|
||
fi
|
||
fi
|
||
press_enter
|
||
configure_paths
|
||
;;
|
||
3)
|
||
echo ""
|
||
echo -n "Enter Console path (e.g., /opt/BetterDeskConsole): "
|
||
read -r new_path
|
||
if [ -n "$new_path" ]; then
|
||
if [ -d "$new_path" ]; then
|
||
CONSOLE_PATH="$new_path"
|
||
print_success "Console path set to: $CONSOLE_PATH"
|
||
else
|
||
print_warning "Directory does not exist: $new_path"
|
||
if confirm "Create this directory?"; then
|
||
mkdir -p "$new_path"
|
||
CONSOLE_PATH="$new_path"
|
||
print_success "Created and set Console path: $CONSOLE_PATH"
|
||
fi
|
||
fi
|
||
fi
|
||
press_enter
|
||
configure_paths
|
||
;;
|
||
4)
|
||
RUSTDESK_PATH="/opt/betterdesk"
|
||
CONSOLE_PATH="/opt/BetterDeskConsole"
|
||
DB_PATH="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
print_success "Paths reset to defaults"
|
||
press_enter
|
||
configure_paths
|
||
;;
|
||
0|"")
|
||
return
|
||
;;
|
||
*)
|
||
print_error "Invalid option"
|
||
press_enter
|
||
configure_paths
|
||
;;
|
||
esac
|
||
}
|
||
|
||
print_status() {
|
||
detect_installation
|
||
detect_architecture
|
||
detect_os
|
||
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ System Status ═══${NC}"
|
||
echo ""
|
||
echo -e " System: ${CYAN}$OS_NAME $OS_VERSION${NC}"
|
||
echo -e " Architecture: ${CYAN}$ARCH_NAME${NC}"
|
||
echo ""
|
||
|
||
echo -e "${WHITE}${BOLD}═══ Configured Paths ═══${NC}"
|
||
echo ""
|
||
echo -e " RustDesk: ${CYAN}$RUSTDESK_PATH${NC}"
|
||
echo -e " Console: ${CYAN}$CONSOLE_PATH${NC}"
|
||
|
||
# Show database type and path/URI
|
||
local diag_db_type="sqlite"
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
diag_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
diag_db_type="${diag_db_type:-sqlite}"
|
||
fi
|
||
if [ "$diag_db_type" = "postgres" ]; then
|
||
local diag_pg_uri
|
||
diag_pg_uri=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
# Mask password in URI for display
|
||
local diag_pg_display
|
||
diag_pg_display=$(echo "$diag_pg_uri" | sed 's|://[^:]*:[^@]*@|://***:***@|')
|
||
echo -e " Database: ${CYAN}PostgreSQL${NC} ($diag_pg_display)"
|
||
else
|
||
echo -e " Database: ${CYAN}SQLite${NC} ($DB_PATH)"
|
||
fi
|
||
echo ""
|
||
|
||
echo -e "${WHITE}${BOLD}═══ Installation Status ═══${NC}"
|
||
echo ""
|
||
|
||
# Installation status
|
||
case "$INSTALL_STATUS" in
|
||
"complete")
|
||
echo -e " Status: ${GREEN}✓ Installed${NC}"
|
||
;;
|
||
"partial")
|
||
echo -e " Status: ${YELLOW}! Partial installation${NC}"
|
||
;;
|
||
"none")
|
||
echo -e " Status: ${RED}✗ Not installed${NC}"
|
||
;;
|
||
esac
|
||
|
||
# Components
|
||
if [ "$BINARIES_OK" = true ]; then
|
||
echo -e " Binaries: ${GREEN}✓ OK${NC}"
|
||
else
|
||
echo -e " Binaries: ${RED}✗ Not found${NC}"
|
||
fi
|
||
|
||
if [ "$DATABASE_OK" = true ]; then
|
||
echo -e " Database: ${GREEN}✓ OK${NC}"
|
||
else
|
||
echo -e " Database: ${RED}✗ Not found${NC}"
|
||
fi
|
||
|
||
if [ -d "$CONSOLE_PATH" ]; then
|
||
case "$CONSOLE_TYPE" in
|
||
nodejs) echo -e " Web Console: ${GREEN}✓ OK${NC} (Node.js)" ;;
|
||
*) echo -e " Web Console: ${GREEN}✓ OK${NC}" ;;
|
||
esac
|
||
else
|
||
echo -e " Web Console: ${RED}✗ Not found${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Services Status ═══${NC}"
|
||
echo ""
|
||
|
||
# Check if using Go server (single binary) or legacy Rust (two binaries)
|
||
if [ "${SERVER_TYPE:-}" = "go" ] || systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
local go_state
|
||
go_state=$(systemctl show betterdesk-server --property=ActiveState --value 2>/dev/null || echo "unknown")
|
||
case "$go_state" in
|
||
active)
|
||
echo -e " BetterDesk Server (Go): ${GREEN}● Active${NC} (Signal + Relay + API)"
|
||
;;
|
||
failed)
|
||
echo -e " BetterDesk Server (Go): ${RED}✗ Failed${NC} (check: journalctl -u betterdesk-server -n 30)"
|
||
;;
|
||
activating)
|
||
echo -e " BetterDesk Server (Go): ${YELLOW}◌ Starting...${NC}"
|
||
;;
|
||
*)
|
||
echo -e " BetterDesk Server (Go): ${RED}○ Inactive${NC} ($go_state)"
|
||
;;
|
||
esac
|
||
else
|
||
# Legacy Rust servers
|
||
if [ "$HBBS_RUNNING" = true ]; then
|
||
echo -e " HBBS (Signal): ${GREEN}● Active${NC} ${YELLOW}(Legacy Rust)${NC}"
|
||
else
|
||
echo -e " HBBS (Signal): ${RED}○ Inactive${NC}"
|
||
fi
|
||
|
||
if [ "$HBBR_RUNNING" = true ]; then
|
||
echo -e " HBBR (Relay): ${GREEN}● Active${NC} ${YELLOW}(Legacy Rust)${NC}"
|
||
else
|
||
echo -e " HBBR (Relay): ${RED}○ Inactive${NC}"
|
||
fi
|
||
fi
|
||
|
||
# Console status with state details
|
||
local console_state
|
||
console_state=$(systemctl show betterdesk-console --property=ActiveState --value 2>/dev/null || echo "unknown")
|
||
case "$console_state" in
|
||
active)
|
||
echo -e " Web Console: ${GREEN}● Active${NC}"
|
||
;;
|
||
failed)
|
||
echo -e " Web Console: ${RED}✗ Failed${NC} (check: journalctl -u betterdesk-console -n 30)"
|
||
;;
|
||
activating)
|
||
echo -e " Web Console: ${YELLOW}◌ Starting...${NC}"
|
||
;;
|
||
*)
|
||
if [ "$CONSOLE_RUNNING" = true ]; then
|
||
echo -e " Web Console: ${GREEN}● Active${NC}"
|
||
else
|
||
echo -e " Web Console: ${RED}○ Inactive${NC} ($console_state)"
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
}
|
||
|
||
#===============================================================================
|
||
# Go Installation and Compilation
|
||
#===============================================================================
|
||
|
||
# Extract numeric semver component (handles "1.25+", "26rc1", etc.).
|
||
_go_version_part() {
|
||
local ver="$1" field="${2:-1}"
|
||
local part
|
||
part=$(echo "$ver" | cut -d'.' -f"$field" | grep -oE '^[0-9]+' | head -1)
|
||
echo "${part:-0}"
|
||
}
|
||
|
||
check_go_installed() {
|
||
if command -v go &> /dev/null; then
|
||
local go_version
|
||
go_version=$(go version | awk '{print $3}' | sed 's/go//')
|
||
local go_major=$(_go_version_part "$go_version" 1)
|
||
local go_minor=$(_go_version_part "$go_version" 2)
|
||
local go_patch=$(_go_version_part "$go_version" 3)
|
||
local min_major=$(_go_version_part "$GO_MIN_VERSION" 1)
|
||
local min_minor=$(_go_version_part "$GO_MIN_VERSION" 2)
|
||
local min_patch=$(_go_version_part "$GO_MIN_VERSION" 3)
|
||
|
||
# Security hardening: reject vulnerable Go 1.26.0 stdlib.
|
||
if [ "$go_major" -eq 1 ] && [ "$go_minor" -eq 26 ] && [ "$go_patch" -eq 0 ]; then
|
||
print_warning "Detected vulnerable Go version $go_version (known stdlib CVEs)."
|
||
return 1
|
||
fi
|
||
|
||
if [ "$go_major" -gt "$min_major" ] ||
|
||
([ "$go_major" -eq "$min_major" ] && [ "$go_minor" -gt "$min_minor" ]) ||
|
||
([ "$go_major" -eq "$min_major" ] && [ "$go_minor" -eq "$min_minor" ] && [ "$go_patch" -ge "$min_patch" ]); then
|
||
return 0
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
install_golang() {
|
||
print_step "Installing Go $GO_MIN_VERSION+..."
|
||
|
||
# Ensure architecture is detected
|
||
if [ -z "$ARCH_NAME" ]; then
|
||
detect_architecture
|
||
fi
|
||
|
||
if check_go_installed; then
|
||
local go_version=$(go version | awk '{print $3}' | sed 's/go//')
|
||
print_info "Go $go_version is already installed"
|
||
return 0
|
||
fi
|
||
|
||
local go_version="$GO_DOWNLOAD_VERSION"
|
||
local go_arch=""
|
||
|
||
case "$ARCH_NAME" in
|
||
x86_64) go_arch="amd64" ;;
|
||
aarch64) go_arch="arm64" ;;
|
||
armv7*) go_arch="armv6l" ;;
|
||
*) print_error "Unsupported architecture: $ARCH_NAME"; return 1 ;;
|
||
esac
|
||
|
||
local go_tarball="go${go_version}.linux-${go_arch}.tar.gz"
|
||
local go_url="https://go.dev/dl/$go_tarball"
|
||
|
||
print_info "Downloading Go $go_version for $go_arch..."
|
||
|
||
cd /tmp
|
||
if command -v wget &> /dev/null; then
|
||
wget -q --show-progress "$go_url" -O "$go_tarball" || wget "$go_url" -O "$go_tarball"
|
||
elif command -v curl &> /dev/null; then
|
||
curl -fSL --progress-bar "$go_url" -o "$go_tarball"
|
||
else
|
||
print_error "Neither wget nor curl available"
|
||
return 1
|
||
fi
|
||
|
||
print_info "Installing Go to /usr/local/go..."
|
||
rm -rf /usr/local/go
|
||
tar -C /usr/local -xzf "$go_tarball"
|
||
rm "$go_tarball"
|
||
|
||
# Add to PATH for current session
|
||
export PATH=$PATH:/usr/local/go/bin
|
||
|
||
# Add to system-wide PATH
|
||
if [ ! -f /etc/profile.d/go.sh ]; then
|
||
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
|
||
chmod +x /etc/profile.d/go.sh
|
||
fi
|
||
|
||
if check_go_installed; then
|
||
print_success "Go $go_version installed successfully"
|
||
return 0
|
||
else
|
||
print_error "Go installation failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Probe a fixed HTTPS host without executing response body (connectivity only).
|
||
# Optional 3rd arg: curl IP family flag ("-4" or "-6"); empty = dual-stack default.
|
||
_go_probe_https() {
|
||
local url="$1"
|
||
local limit="${2:-$GO_MODULE_PREFLIGHT_TIMEOUT}"
|
||
local ip_flag="${3:-}"
|
||
|
||
if command -v curl &> /dev/null; then
|
||
# No -f: any HTTP response means the TCP/TLS path works (404 is fine).
|
||
# shellcheck disable=SC2086
|
||
curl -sS -o /dev/null --connect-timeout "$limit" --max-time "$limit" $ip_flag "$url"
|
||
return $?
|
||
fi
|
||
if command -v wget &> /dev/null; then
|
||
# wget has no reliable -4/-6 on all distros; dual-stack only.
|
||
wget -q --spider --timeout="$limit" --tries=1 "$url"
|
||
return $?
|
||
fi
|
||
return 2
|
||
}
|
||
|
||
# Read a sysctl value or echo "unknown".
|
||
_go_sysctl_get() {
|
||
local key="$1"
|
||
if command -v sysctl &> /dev/null; then
|
||
sysctl -n "$key" 2>/dev/null || echo "unknown"
|
||
else
|
||
echo "unknown"
|
||
fi
|
||
}
|
||
|
||
# Temporarily disable IPv6 for Go dual-stack dials on broken-IPv6 VMs (save/restore).
|
||
_go_ipv4_force_begin() {
|
||
GO_IPV4_SAVED_ALL=""
|
||
GO_IPV4_SAVED_DEFAULT=""
|
||
GO_IPV4_APPLIED=0
|
||
|
||
if [ "${GO_FORCE_IPV4:-0}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
if ! command -v sysctl &> /dev/null; then
|
||
print_warning "Broken IPv6 detected but sysctl unavailable; cannot force IPv4 for Go"
|
||
return 0
|
||
fi
|
||
|
||
GO_IPV4_SAVED_ALL=$(_go_sysctl_get net.ipv6.conf.all.disable_ipv6)
|
||
GO_IPV4_SAVED_DEFAULT=$(_go_sysctl_get net.ipv6.conf.default.disable_ipv6)
|
||
|
||
if sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1 \
|
||
&& sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1; then
|
||
GO_IPV4_APPLIED=1
|
||
print_info "Temporarily disabled IPv6 for Go module download (broken IPv6 path detected)"
|
||
else
|
||
print_warning "Could not disable IPv6 via sysctl; Go may still hang on AAAA dials"
|
||
fi
|
||
}
|
||
|
||
_go_ipv4_force_end() {
|
||
if [ "${GO_IPV4_APPLIED:-0}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
if [ -n "${GO_IPV4_SAVED_ALL}" ] && [ "${GO_IPV4_SAVED_ALL}" != "unknown" ]; then
|
||
sysctl -w "net.ipv6.conf.all.disable_ipv6=${GO_IPV4_SAVED_ALL}" >/dev/null 2>&1 || true
|
||
fi
|
||
if [ -n "${GO_IPV4_SAVED_DEFAULT}" ] && [ "${GO_IPV4_SAVED_DEFAULT}" != "unknown" ]; then
|
||
sysctl -w "net.ipv6.conf.default.disable_ipv6=${GO_IPV4_SAVED_DEFAULT}" >/dev/null 2>&1 || true
|
||
fi
|
||
GO_IPV4_APPLIED=0
|
||
print_info "Restored previous IPv6 sysctl settings"
|
||
}
|
||
|
||
# Drop leftover partial/lock files from a previous Ctrl+C during go mod download.
|
||
_go_clear_stale_module_partials() {
|
||
local modcache
|
||
modcache=$(go env GOMODCACHE 2>/dev/null || true)
|
||
if [ -z "$modcache" ] || [ ! -d "$modcache" ]; then
|
||
return 0
|
||
fi
|
||
local cleared=0
|
||
cleared=$(find "$modcache" \( -name '*.partial' -o -name '*.lock' \) 2>/dev/null | wc -l | tr -d ' ')
|
||
if [ "${cleared:-0}" -gt 0 ] 2>/dev/null; then
|
||
print_warning "Removing ${cleared} stale Go module cache lock/partial file(s) from a prior interrupted download"
|
||
find "$modcache" \( -name '*.partial' -o -name '*.lock' \) -delete 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# Fail fast when proxy.golang.org / sum.golang.org are unreachable.
|
||
# Prefer IPv4 probes; set GO_FORCE_IPV4=1 when IPv6 is broken but IPv4 works.
|
||
_go_module_network_preflight() {
|
||
local limit="${GO_MODULE_PREFLIGHT_TIMEOUT}"
|
||
local probe_status=0
|
||
local ipv4_ok=0
|
||
local ipv6_ok=0
|
||
|
||
GO_FORCE_IPV4=0
|
||
print_info "Checking HTTPS reachability of Go module endpoints (${limit}s)..."
|
||
|
||
if ! command -v curl &> /dev/null && ! command -v wget &> /dev/null; then
|
||
print_warning "Neither curl nor wget available; skipping Go module network preflight"
|
||
return 0
|
||
fi
|
||
|
||
if command -v curl &> /dev/null; then
|
||
if _go_probe_https "https://proxy.golang.org/" "$limit" "-4"; then
|
||
ipv4_ok=1
|
||
print_success "Reachable via IPv4: proxy.golang.org"
|
||
else
|
||
print_error "Cannot reach https://proxy.golang.org/ via IPv4 within ${limit}s"
|
||
fi
|
||
|
||
# Short IPv6 probe — failure here is common on GCP and is not fatal if IPv4 works.
|
||
if _go_probe_https "https://proxy.golang.org/" 5 "-6"; then
|
||
ipv6_ok=1
|
||
print_success "Reachable via IPv6: proxy.golang.org"
|
||
else
|
||
print_warning "IPv6 path to proxy.golang.org failed (will prefer IPv4 for Go if IPv4 works)"
|
||
fi
|
||
|
||
if [ "$ipv4_ok" -eq 1 ] && [ "$ipv6_ok" -eq 0 ]; then
|
||
GO_FORCE_IPV4=1
|
||
print_info "Broken IPv6 detected — Go module download will temporarily disable IPv6"
|
||
fi
|
||
|
||
if [ "$ipv4_ok" -ne 1 ]; then
|
||
# Last resort: dual-stack (some hosts lack curl -4).
|
||
if _go_probe_https "https://proxy.golang.org/" "$limit"; then
|
||
print_success "Reachable (dual-stack): proxy.golang.org"
|
||
else
|
||
probe_status=1
|
||
fi
|
||
fi
|
||
|
||
if ! _go_probe_https "https://sum.golang.org/" "$limit" "-4" \
|
||
&& ! _go_probe_https "https://sum.golang.org/" "$limit"; then
|
||
print_error "Cannot reach https://sum.golang.org/ within ${limit}s"
|
||
probe_status=1
|
||
else
|
||
print_success "Reachable: sum.golang.org"
|
||
fi
|
||
|
||
# GET a tiny known proxy path so HEAD-only reachability cannot hide a hung download path.
|
||
if [ "$probe_status" -eq 0 ]; then
|
||
print_info "Probing Go module proxy GET (sample @v/list)..."
|
||
if ! curl -4 -sS -o /dev/null --connect-timeout "$limit" --max-time 30 \
|
||
"https://proxy.golang.org/github.com/google/uuid/@v/list"; then
|
||
print_error "Go module proxy GET probe failed within 30s"
|
||
print_error "HEAD may work while module downloads hang — check firewall/DPI/IPv6"
|
||
probe_status=1
|
||
else
|
||
print_success "Go module proxy GET probe OK"
|
||
fi
|
||
fi
|
||
else
|
||
if ! _go_probe_https "https://proxy.golang.org/" "$limit"; then
|
||
print_error "Cannot reach https://proxy.golang.org/ within ${limit}s"
|
||
probe_status=1
|
||
else
|
||
print_success "Reachable: proxy.golang.org"
|
||
fi
|
||
if ! _go_probe_https "https://sum.golang.org/" "$limit"; then
|
||
print_error "Cannot reach https://sum.golang.org/ within ${limit}s"
|
||
probe_status=1
|
||
else
|
||
print_success "Reachable: sum.golang.org"
|
||
fi
|
||
fi
|
||
|
||
if [ "$probe_status" -ne 0 ]; then
|
||
print_error "Outbound HTTPS to the Go module proxy/checksum DB is blocked or timing out"
|
||
print_error "On cloud VMs check DNS, firewall/egress, IPv6 blackholes, and GOPROXY — then retry"
|
||
print_error "Override example: GOPROXY=https://proxy.golang.org,direct"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Kill a process group (negative PGID) with TERM then KILL.
|
||
_go_kill_process_group() {
|
||
local pgid="$1"
|
||
local label="${2:-process group}"
|
||
|
||
if [ -z "$pgid" ] || [ "$pgid" -le 1 ] 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
print_error "Stopping ${label} (PGID ${pgid}) with SIGTERM..."
|
||
kill -TERM -- "-${pgid}" 2>/dev/null || kill -TERM "$pgid" 2>/dev/null || true
|
||
sleep 2
|
||
if kill -0 "$pgid" 2>/dev/null; then
|
||
print_error "Still alive — sending SIGKILL to PGID ${pgid}"
|
||
kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "$pgid" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# Run go mod download with a hard bash process-group deadline (primary guard).
|
||
# GNU timeout alone was insufficient on some cloud VMs (#371).
|
||
_run_go_mod_download_bounded() {
|
||
local deadline="${GO_MODULE_DOWNLOAD_TIMEOUT}"
|
||
local kill_after=15
|
||
local status=0
|
||
local download_pid=""
|
||
local elapsed=0
|
||
local heartbeat_pid=""
|
||
|
||
_go_ipv4_force_begin
|
||
_go_clear_stale_module_partials
|
||
|
||
# Prefer IPv4 for Go's resolver when the host has broken AAAA routes (#371).
|
||
export GODEBUG="${GODEBUG:+$GODEBUG,}netdns=go"
|
||
|
||
# Heartbeat — no `local` inside non-function subshell (set -e).
|
||
(
|
||
elapsed=0
|
||
while true; do
|
||
sleep 15
|
||
elapsed=$((elapsed + 15))
|
||
printf 'ℹ still downloading Go modules... %ss elapsed (watchdog active)\n' "$elapsed" >&2
|
||
done
|
||
) &
|
||
heartbeat_pid=$!
|
||
|
||
# Run go as a direct background child of this shell (same user/session).
|
||
# Prior setsid/pidfile races could exit the waiter early, restore IPv6, and
|
||
# report success while go was still hung or already dead (#371 follow-up).
|
||
(
|
||
echo "go mod download child starting" >&2
|
||
exec go mod download -x
|
||
) &
|
||
download_pid=$!
|
||
|
||
print_info "go mod download started (PID ${download_pid}, deadline ${deadline}s)"
|
||
|
||
elapsed=0
|
||
while kill -0 "$download_pid" 2>/dev/null; do
|
||
if [ "$elapsed" -ge "$deadline" ]; then
|
||
print_error "Go module download exceeded ${deadline}s"
|
||
kill -TERM "$download_pid" 2>/dev/null || true
|
||
sleep "$kill_after"
|
||
if kill -0 "$download_pid" 2>/dev/null; then
|
||
kill -KILL "$download_pid" 2>/dev/null || true
|
||
fi
|
||
status=124
|
||
break
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
|
||
if [ "$status" -ne 124 ]; then
|
||
if wait "$download_pid"; then
|
||
status=0
|
||
else
|
||
status=$?
|
||
fi
|
||
else
|
||
wait "$download_pid" 2>/dev/null || true
|
||
fi
|
||
|
||
if [ -n "$heartbeat_pid" ]; then
|
||
kill "$heartbeat_pid" 2>/dev/null || true
|
||
wait "$heartbeat_pid" 2>/dev/null || true
|
||
fi
|
||
|
||
# Keep IPv6 disabled until we confirm the module graph is readable.
|
||
if [ "$status" -eq 0 ]; then
|
||
if ! go list -m all >/dev/null 2>&1; then
|
||
print_error "go mod download reported success but 'go list -m all' failed"
|
||
status=1
|
||
fi
|
||
fi
|
||
|
||
_go_ipv4_force_end
|
||
return "$status"
|
||
}
|
||
|
||
compile_go_server() {
|
||
print_step "Compiling BetterDesk Go server..."
|
||
|
||
if [ ! -d "$GO_SERVER_SOURCE" ]; then
|
||
print_error "Go server source not found: $GO_SERVER_SOURCE"
|
||
return 1
|
||
fi
|
||
|
||
# Ensure Go is available
|
||
export PATH=$PATH:/usr/local/go/bin
|
||
|
||
if ! check_go_installed; then
|
||
if ! install_golang; then
|
||
print_error "Go is required for compilation"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
cd "$GO_SERVER_SOURCE"
|
||
|
||
# Clean previous builds
|
||
rm -f betterdesk-server betterdesk-server-linux-*
|
||
|
||
# Build
|
||
print_info "Building BetterDesk server for $ARCH_NAME..."
|
||
local output_name="betterdesk-server"
|
||
local go_bin
|
||
go_bin=$(command -v go)
|
||
|
||
# Force local toolchain for native compile so host GOTOOLCHAIN=auto cannot
|
||
# silently download another toolchain mid-install (hang risk on cloud VMs).
|
||
# Override with BETTERDESK_GOTOOLCHAIN if needed. GOPROXY/GOSUMDB still honour env.
|
||
export GOTOOLCHAIN="${BETTERDESK_GOTOOLCHAIN:-local}"
|
||
export GOPROXY="${GOPROXY:-https://proxy.golang.org,direct}"
|
||
export GOSUMDB="${GOSUMDB:-sum.golang.org}"
|
||
|
||
print_info "Using $($go_bin version 2>/dev/null || echo 'unknown go')"
|
||
print_info "GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB} GOTOOLCHAIN=${GOTOOLCHAIN}"
|
||
|
||
if ! _go_module_network_preflight; then
|
||
return 1
|
||
fi
|
||
|
||
# Download dependencies. Keep this visible and hard-bounded (bash PGID watchdog).
|
||
print_info "Downloading Go modules (timeout: ${GO_MODULE_DOWNLOAD_TIMEOUT}s, kill-after: 15s, setsid watchdog)..."
|
||
local module_download_status=0
|
||
# Keep the command in the if condition so `set -e` does not abort before
|
||
# we can capture the actual watchdog/download exit status. Do not use `!`
|
||
# here: inside its branch `$?` would be the status of the negation (0).
|
||
if _run_go_mod_download_bounded; then
|
||
module_download_status=0
|
||
else
|
||
module_download_status=$?
|
||
fi
|
||
|
||
if [ "$module_download_status" -ne 0 ]; then
|
||
if [ "$module_download_status" -eq 124 ] || [ "$module_download_status" -eq 137 ]; then
|
||
print_error "Go module download timed out after ${GO_MODULE_DOWNLOAD_TIMEOUT}s"
|
||
else
|
||
print_error "Go module download failed (exit code ${module_download_status})"
|
||
fi
|
||
print_error "Check DNS, outbound HTTPS to proxy.golang.org / sum.golang.org / go.dev, firewall and GOPROXY, then retry"
|
||
print_error "On GCP/cloud VMs with broken IPv6, confirm IPv4 works: curl -4 -I https://proxy.golang.org/"
|
||
return 1
|
||
fi
|
||
|
||
# Build with optimizations
|
||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=${VERSION}" -o "$output_name" .
|
||
|
||
if [ -f "$output_name" ]; then
|
||
chmod +x "$output_name"
|
||
local size=$(du -h "$output_name" | cut -f1)
|
||
print_success "Compiled: $output_name ($size)"
|
||
return 0
|
||
else
|
||
print_error "Compilation failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
#===============================================================================
|
||
# Binary Verification Functions
|
||
#===============================================================================
|
||
|
||
verify_go_binary() {
|
||
local binary_path="$1"
|
||
|
||
if [ -z "$binary_path" ]; then
|
||
binary_path="$GO_SERVER_SOURCE/betterdesk-server"
|
||
fi
|
||
|
||
if [ ! -f "$binary_path" ]; then
|
||
# Check installed location
|
||
binary_path="$RUSTDESK_PATH/betterdesk-server"
|
||
fi
|
||
|
||
if [ ! -f "$binary_path" ]; then
|
||
return 1
|
||
fi
|
||
|
||
# Verify it's executable
|
||
if [ -x "$binary_path" ]; then
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
verify_binaries() {
|
||
print_step "Verifying BetterDesk server..."
|
||
|
||
if [ "$SKIP_VERIFY" = true ]; then
|
||
print_warning "Verification skipped (--skip-verify)"
|
||
return 0
|
||
fi
|
||
|
||
# Check for precompiled binary
|
||
local found=false
|
||
|
||
if [ -f "$GO_SERVER_SOURCE/betterdesk-server" ]; then
|
||
if verify_go_binary "$GO_SERVER_SOURCE/betterdesk-server"; then
|
||
local size=$(du -h "$GO_SERVER_SOURCE/betterdesk-server" | cut -f1)
|
||
print_success "Found compiled binary in source directory ($size)"
|
||
found=true
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
if verify_go_binary "$RUSTDESK_PATH/betterdesk-server"; then
|
||
local size=$(du -h "$RUSTDESK_PATH/betterdesk-server" | cut -f1)
|
||
print_success "Found installed binary ($size)"
|
||
found=true
|
||
fi
|
||
fi
|
||
|
||
if [ "$found" = false ]; then
|
||
print_warning "No BetterDesk server binary found"
|
||
print_info "Binary will be compiled during installation"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
#===============================================================================
|
||
# Installation Functions
|
||
#===============================================================================
|
||
|
||
install_dependencies() {
|
||
print_step "Installing dependencies..."
|
||
|
||
if command -v apt-get &> /dev/null; then
|
||
apt-get update -qq
|
||
apt-get install -y -qq python3 python3-pip python3-venv sqlite3 curl wget openssl build-essential
|
||
elif command -v dnf &> /dev/null; then
|
||
dnf install -y -q python3 python3-pip sqlite curl wget openssl gcc gcc-c++ make
|
||
elif command -v yum &> /dev/null; then
|
||
yum install -y -q python3 python3-pip sqlite curl wget openssl gcc gcc-c++ make
|
||
elif command -v pacman &> /dev/null; then
|
||
pacman -Sy --noconfirm python python-pip sqlite curl wget openssl base-devel
|
||
else
|
||
print_warning "Unknown package manager. Make sure Python 3 and SQLite are installed."
|
||
fi
|
||
|
||
print_success "Dependencies installed"
|
||
}
|
||
|
||
#===============================================================================
|
||
# PostgreSQL Installation Functions
|
||
#===============================================================================
|
||
|
||
install_postgresql() {
|
||
print_step "Installing PostgreSQL..."
|
||
|
||
if command -v psql &> /dev/null; then
|
||
local pg_version=$(psql --version | grep -oP '\d+' | head -1)
|
||
print_success "PostgreSQL $pg_version already installed"
|
||
return 0
|
||
fi
|
||
|
||
if command -v apt-get &> /dev/null; then
|
||
apt-get update -qq
|
||
apt-get install -y -qq postgresql postgresql-contrib
|
||
elif command -v dnf &> /dev/null; then
|
||
dnf install -y -q postgresql-server postgresql
|
||
postgresql-setup --initdb 2>/dev/null || true
|
||
elif command -v yum &> /dev/null; then
|
||
yum install -y -q postgresql-server postgresql
|
||
postgresql-setup initdb 2>/dev/null || true
|
||
elif command -v pacman &> /dev/null; then
|
||
pacman -Sy --noconfirm postgresql
|
||
su - postgres -c "initdb -D /var/lib/postgres/data" 2>/dev/null || true
|
||
else
|
||
print_error "Cannot install PostgreSQL automatically."
|
||
print_info "Please install PostgreSQL manually and run the script again."
|
||
return 1
|
||
fi
|
||
|
||
# Start PostgreSQL service
|
||
systemctl start postgresql 2>/dev/null || service postgresql start 2>/dev/null || true
|
||
systemctl enable postgresql 2>/dev/null || true
|
||
|
||
# Verify installation
|
||
if command -v psql &> /dev/null; then
|
||
print_success "PostgreSQL installed and started"
|
||
return 0
|
||
else
|
||
print_error "PostgreSQL installation failed!"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
setup_postgresql_database() {
|
||
print_step "Setting up PostgreSQL database for BetterDesk..."
|
||
|
||
if ! is_valid_pg_identifier "$POSTGRESQL_USER"; then
|
||
print_error "Invalid PostgreSQL username: $POSTGRESQL_USER"
|
||
print_info "Allowed pattern: ^[A-Za-z_][A-Za-z0-9_]{0,62}$"
|
||
return 1
|
||
fi
|
||
if ! is_valid_pg_identifier "$POSTGRESQL_DB"; then
|
||
print_error "Invalid PostgreSQL database name: $POSTGRESQL_DB"
|
||
print_info "Allowed pattern: ^[A-Za-z_][A-Za-z0-9_]{0,62}$"
|
||
return 1
|
||
fi
|
||
|
||
# Generate password if not set
|
||
if [ -z "$POSTGRESQL_PASS" ]; then
|
||
# SECURITY (audit fix M-05, 2026-04-10): use hex (4 bits/char, no
|
||
# alphabet shrinking) instead of base64+tr+truncate which lost a few
|
||
# entropy bits per character.
|
||
POSTGRESQL_PASS=$(openssl rand -hex 16)
|
||
print_info "Generated PostgreSQL password"
|
||
fi
|
||
|
||
# Check if PostgreSQL is running
|
||
if ! systemctl is-active --quiet postgresql 2>/dev/null; then
|
||
systemctl start postgresql 2>/dev/null || service postgresql start 2>/dev/null
|
||
sleep 2
|
||
fi
|
||
|
||
# Create user and database
|
||
local pg_pass_sql
|
||
pg_pass_sql=$(sql_escape_literal "$POSTGRESQL_PASS")
|
||
|
||
print_step "Creating PostgreSQL user '$POSTGRESQL_USER'..."
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE USER \"$POSTGRESQL_USER\" WITH PASSWORD '$pg_pass_sql' CREATEDB;" 2>/dev/null || {
|
||
print_warning "User might already exist, trying to update password..."
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "ALTER USER \"$POSTGRESQL_USER\" WITH PASSWORD '$pg_pass_sql';" 2>/dev/null || true
|
||
}
|
||
|
||
print_step "Creating PostgreSQL database '$POSTGRESQL_DB'..."
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$POSTGRESQL_DB\" OWNER \"$POSTGRESQL_USER\";" 2>/dev/null || {
|
||
print_warning "Database might already exist"
|
||
}
|
||
|
||
# Build connection URI
|
||
POSTGRESQL_URI="postgres://$POSTGRESQL_USER:$POSTGRESQL_PASS@$POSTGRESQL_HOST:$POSTGRESQL_PORT/$POSTGRESQL_DB?sslmode=disable"
|
||
|
||
# Test connection
|
||
print_step "Testing PostgreSQL connection..."
|
||
if PGPASSWORD="$POSTGRESQL_PASS" psql -U "$POSTGRESQL_USER" -h "$POSTGRESQL_HOST" -p "$POSTGRESQL_PORT" -d "$POSTGRESQL_DB" -c "SELECT 1;" &>/dev/null; then
|
||
print_success "PostgreSQL connection successful!"
|
||
print_info "Connection URI: postgres://$POSTGRESQL_USER:****@$POSTGRESQL_HOST:$POSTGRESQL_PORT/$POSTGRESQL_DB"
|
||
else
|
||
print_error "PostgreSQL connection failed!"
|
||
print_info "Check PostgreSQL pg_hba.conf for local connections"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
choose_database_type() {
|
||
if [ "$AUTO_MODE" = true ]; then
|
||
# In auto mode, use environment variable or default to SQLite
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
print_info "Auto mode: Using PostgreSQL"
|
||
return 0
|
||
else
|
||
print_info "Auto mode: Using SQLite (default)"
|
||
USE_POSTGRESQL="false"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
local _menu_items=(
|
||
$'SQLite (default)\tSingle-file DB, zero setup, good for <=100 devices'
|
||
$'PostgreSQL (production)\tPooled SQL DB, multi-server / >100 devices / HA'
|
||
)
|
||
local _menu_returns=( 1 2 )
|
||
menu_choose "Select Database Type" "SQLite is recommended for most installs"
|
||
local db_choice="${MENU_CHOICE:-1}"
|
||
|
||
case $db_choice in
|
||
2)
|
||
USE_POSTGRESQL="true"
|
||
print_info "Selected: PostgreSQL"
|
||
|
||
# Ask for PostgreSQL details or use defaults
|
||
echo ""
|
||
read -p "PostgreSQL host [$POSTGRESQL_HOST]: " pg_host
|
||
POSTGRESQL_HOST="${pg_host:-$POSTGRESQL_HOST}"
|
||
|
||
read -p "PostgreSQL port [$POSTGRESQL_PORT]: " pg_port
|
||
POSTGRESQL_PORT="${pg_port:-$POSTGRESQL_PORT}"
|
||
|
||
read -p "PostgreSQL database [$POSTGRESQL_DB]: " pg_db
|
||
POSTGRESQL_DB="${pg_db:-$POSTGRESQL_DB}"
|
||
|
||
read -p "PostgreSQL user [$POSTGRESQL_USER]: " pg_user
|
||
POSTGRESQL_USER="${pg_user:-$POSTGRESQL_USER}"
|
||
|
||
read -sp "PostgreSQL password (leave empty to generate): " pg_pass
|
||
echo ""
|
||
POSTGRESQL_PASS="${pg_pass:-}"
|
||
;;
|
||
*)
|
||
USE_POSTGRESQL="false"
|
||
print_info "Selected: SQLite"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
migrate_sqlite_to_postgresql() {
|
||
print_step "Migrating existing SQLite data to PostgreSQL..."
|
||
|
||
local sqlite_db="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
|
||
if [ ! -f "$sqlite_db" ]; then
|
||
print_info "No existing SQLite database found, skipping migration"
|
||
return 0
|
||
fi
|
||
|
||
# Find migration binary
|
||
local migrate_bin=""
|
||
if [ -f "$SCRIPT_DIR/betterdesk-server/tools/migrate/migrate-linux-amd64" ]; then
|
||
migrate_bin="$SCRIPT_DIR/betterdesk-server/tools/migrate/migrate-linux-amd64"
|
||
elif [ -f "$SCRIPT_DIR/tools/migrate/migrate-linux-amd64" ]; then
|
||
migrate_bin="$SCRIPT_DIR/tools/migrate/migrate-linux-amd64"
|
||
elif [ -f "/opt/betterdesk-go/migrate" ]; then
|
||
migrate_bin="/opt/betterdesk-go/migrate"
|
||
fi
|
||
|
||
# Try to compile migration tool from source if not found or outdated
|
||
if [ -z "$migrate_bin" ] && command -v go &>/dev/null; then
|
||
local migrate_src="$SCRIPT_DIR/betterdesk-server/tools/migrate"
|
||
if [ -d "$migrate_src" ]; then
|
||
print_info "Compiling migration tool from source..."
|
||
if (cd "$SCRIPT_DIR/betterdesk-server" && go build -o "tools/migrate/migrate-linux-amd64" ./tools/migrate/) 2>&1; then
|
||
migrate_bin="$migrate_src/migrate-linux-amd64"
|
||
print_success "Migration tool compiled successfully"
|
||
else
|
||
print_warning "Failed to compile migration tool"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$migrate_bin" ]; then
|
||
print_warning "Migration binary not found, skipping automatic migration"
|
||
print_info "You can migrate manually using: M -> 3 (SQLite → PostgreSQL)"
|
||
return 0
|
||
fi
|
||
|
||
chmod +x "$migrate_bin"
|
||
|
||
# Verify binary supports -mode flag (in case of outdated binary)
|
||
if ! "$migrate_bin" -mode backup -src /dev/null 2>&1 | grep -qv "flag provided but not defined"; then
|
||
if "$migrate_bin" -mode backup -src /dev/null 2>&1 | grep -q "flag provided but not defined"; then
|
||
print_warning "Migration binary is outdated (missing -mode flag)"
|
||
print_info "Rebuild with: cd betterdesk-server && go build -o tools/migrate/migrate-linux-amd64 ./tools/migrate/"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Check if SQLite has data
|
||
local peer_count
|
||
peer_count=$(sqlite3 "$sqlite_db" "SELECT COUNT(*) FROM peer;" 2>/dev/null || echo "0")
|
||
|
||
if [ "$peer_count" -gt 0 ]; then
|
||
print_info "Found $peer_count devices in SQLite database"
|
||
|
||
if [ "$AUTO_MODE" = true ] || confirm "Migrate existing data to PostgreSQL?"; then
|
||
print_step "Creating backup before migration..."
|
||
"$migrate_bin" -mode backup -src "$sqlite_db" 2>&1 || true
|
||
|
||
print_step "Running SQLite → PostgreSQL migration (nodejs2go mode)..."
|
||
if "$migrate_bin" -mode nodejs2go -src "$sqlite_db" -dst "$POSTGRESQL_URI" 2>&1; then
|
||
print_success "Migration completed! $peer_count devices migrated."
|
||
else
|
||
print_warning "Migration had issues, check output above"
|
||
fi
|
||
fi
|
||
else
|
||
print_info "SQLite database is empty, no migration needed"
|
||
fi
|
||
}
|
||
|
||
#===============================================================================
|
||
# Node.js Installation Functions
|
||
#===============================================================================
|
||
|
||
install_nodejs() {
|
||
print_step "Checking Node.js installation..."
|
||
|
||
# Check if Node.js is already installed and version is sufficient
|
||
if command -v node &> /dev/null; then
|
||
local node_version=$(node --version | sed 's/v//' | cut -d'.' -f1)
|
||
if [ "$node_version" -ge 22 ]; then
|
||
print_success "Node.js v$(node --version) already installed"
|
||
return 0
|
||
else
|
||
print_warning "Node.js version $node_version is too old (need 22+). Upgrading..."
|
||
fi
|
||
fi
|
||
|
||
# Keep new bare-metal console installs on Node 22 while Node 24.19.x
|
||
# cleanup-hook crashes affect native better-sqlite3 statement finalizers.
|
||
print_step "Installing Node.js 22 LTS..."
|
||
|
||
# Detect OS and install Node.js. The NodeSource setup script is downloaded
|
||
# to a temp file and validated before execution (H5 audit fix): we do NOT
|
||
# pipe `curl | bash` blindly. Optional pinning: set $NODESOURCE_SHA256 to
|
||
# require an exact SHA-256 match before running the installer.
|
||
_fetch_and_run_nodesource() {
|
||
local url="$1"
|
||
local tmp
|
||
tmp=$(mktemp --suffix=.sh) || { print_error "mktemp failed"; return 1; }
|
||
trap "rm -f '$tmp'" RETURN
|
||
|
||
if ! curl -fsSL --max-time 60 --proto '=https' --tlsv1.2 -o "$tmp" "$url"; then
|
||
print_error "Failed to download NodeSource setup script from $url"
|
||
return 1
|
||
fi
|
||
|
||
# Size sanity check: NodeSource setup script is ~15-40 KB. Reject anything
|
||
# outside [1 KB, 500 KB] (catches HTML error pages and tampered payloads).
|
||
local size
|
||
size=$(stat -c%s "$tmp" 2>/dev/null || wc -c <"$tmp")
|
||
if [ "${size:-0}" -lt 1024 ] || [ "${size:-0}" -gt 512000 ]; then
|
||
print_error "Downloaded NodeSource script has unexpected size (${size} bytes). Aborting."
|
||
return 1
|
||
fi
|
||
|
||
# Header sanity check: must be a bash/sh script.
|
||
local first_line
|
||
first_line=$(head -n 1 "$tmp")
|
||
case "$first_line" in
|
||
"#!/bin/bash"*|"#!/usr/bin/env bash"*|"#!/bin/sh"*|"#!/usr/bin/env sh"*) ;;
|
||
*)
|
||
print_error "Downloaded NodeSource script has unexpected shebang: '$first_line'"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
# Optional pinned SHA-256 verification.
|
||
local actual_sha
|
||
if command -v sha256sum &> /dev/null; then
|
||
actual_sha=$(sha256sum "$tmp" | awk '{print $1}')
|
||
elif command -v shasum &> /dev/null; then
|
||
actual_sha=$(shasum -a 256 "$tmp" | awk '{print $1}')
|
||
fi
|
||
if [ -n "$actual_sha" ]; then
|
||
print_info "NodeSource setup script SHA-256: $actual_sha"
|
||
if [ -n "${NODESOURCE_SHA256:-}" ] && [ "$actual_sha" != "$NODESOURCE_SHA256" ]; then
|
||
print_error "NodeSource SHA-256 mismatch (expected $NODESOURCE_SHA256, got $actual_sha)"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
bash "$tmp"
|
||
}
|
||
|
||
# Detect OS and install Node.js
|
||
if command -v apt-get &> /dev/null; then
|
||
# Debian/Ubuntu - use NodeSource
|
||
_fetch_and_run_nodesource "https://deb.nodesource.com/setup_22.x" || return 1
|
||
apt-get install -y -qq nodejs
|
||
elif command -v dnf &> /dev/null; then
|
||
# Fedora/RHEL 8+
|
||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_22.x" || return 1
|
||
dnf install -y -q nodejs
|
||
elif command -v yum &> /dev/null; then
|
||
# RHEL/CentOS 7
|
||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_22.x" || return 1
|
||
yum install -y -q nodejs
|
||
elif command -v pacman &> /dev/null; then
|
||
# Arch Linux
|
||
pacman -Sy --noconfirm nodejs npm
|
||
elif command -v apk &> /dev/null; then
|
||
# Alpine Linux
|
||
apk add --no-cache nodejs npm
|
||
else
|
||
print_error "Cannot install Node.js automatically. Please install Node.js 22+ manually."
|
||
return 1
|
||
fi
|
||
|
||
# Verify installation
|
||
if command -v node &> /dev/null; then
|
||
print_success "Node.js $(node --version) installed"
|
||
print_info "npm $(npm --version)"
|
||
return 0
|
||
else
|
||
print_error "Node.js installation failed!"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
install_nodejs_console() {
|
||
print_step "Installing Node.js Web Console..."
|
||
|
||
# Install Node.js if not present
|
||
if ! install_nodejs; then
|
||
print_error "Cannot proceed without Node.js"
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p "$CONSOLE_PATH"
|
||
|
||
# Check for web-nodejs folder first, then web folder
|
||
local source_folder=""
|
||
if [ -d "$SCRIPT_DIR/web-nodejs" ]; then
|
||
source_folder="$SCRIPT_DIR/web-nodejs"
|
||
print_info "Found Node.js console in web-nodejs/"
|
||
elif [ -d "$SCRIPT_DIR/web" ] && [ -f "$SCRIPT_DIR/web/server.js" ]; then
|
||
source_folder="$SCRIPT_DIR/web"
|
||
print_info "Found Node.js console in web/"
|
||
else
|
||
print_error "Node.js web console not found!"
|
||
print_info "Expected: $SCRIPT_DIR/web-nodejs/ or $SCRIPT_DIR/web/server.js"
|
||
return 1
|
||
fi
|
||
|
||
# Copy web files (glob * skips dotfiles — .env.example is required by merge-env.js, #166)
|
||
cp -r "$source_folder/"* "$CONSOLE_PATH/"
|
||
if [ -f "$source_folder/.env.example" ]; then
|
||
cp -a "$source_folder/.env.example" "$CONSOLE_PATH/.env.example"
|
||
fi
|
||
if [ -f "$SCRIPT_DIR/VERSION" ]; then
|
||
cp -a "$SCRIPT_DIR/VERSION" "$CONSOLE_PATH/VERSION" 2>/dev/null || true
|
||
fi
|
||
|
||
# Install npm dependencies
|
||
print_step "Installing npm dependencies..."
|
||
cd "$CONSOLE_PATH"
|
||
|
||
# Install npm dependencies with proper error handling
|
||
local npm_log="/tmp/betterdesk_npm_install.log"
|
||
if ! npm install --production > "$npm_log" 2>&1; then
|
||
print_error "npm install failed! Check log:"
|
||
tail -20 "$npm_log"
|
||
print_info "Full log: $npm_log"
|
||
return 1
|
||
fi
|
||
rm -f "$npm_log"
|
||
echo ""
|
||
|
||
# Best-effort install of node-pty for Server Management terminal (BETA).
|
||
# node-pty is an optional dependency: if the native build fails the
|
||
# console falls back to plain pipe spawn (no PTY).
|
||
print_step "Installing optional node-pty (Server Management terminal — BETA)..."
|
||
if npm install --no-audit --no-fund --no-save node-pty >>"$npm_log" 2>&1; then
|
||
print_success "node-pty installed (real PTY available)"
|
||
else
|
||
print_warn "node-pty install failed — Server Management terminal will use pipe fallback"
|
||
fi
|
||
rm -f "$npm_log"
|
||
|
||
# Do not suggest a broad systemctl/journalctl sudoers rule here. The
|
||
# privileged update broker is the only supported panel elevation path.
|
||
echo ""
|
||
|
||
# Create data directory for databases
|
||
mkdir -p "$CONSOLE_PATH/data"
|
||
|
||
# Fresh install only when no existing panel state (issue #158 — never reset passwords on update).
|
||
local is_fresh=false
|
||
if [ ! -f "$CONSOLE_PATH/.env" ] && [ ! -f "$CONSOLE_PATH/data/auth.db" ]; then
|
||
is_fresh=true
|
||
fi
|
||
|
||
if [ "$is_fresh" = true ]; then
|
||
if [ -f "$CONSOLE_PATH/data/auth.db" ]; then
|
||
print_info "Removing old auth database (fresh install)..."
|
||
rm -f "$CONSOLE_PATH/data/auth.db" "$CONSOLE_PATH/data/auth.db-wal" "$CONSOLE_PATH/data/auth.db-shm"
|
||
fi
|
||
if [ -z "$ADMIN_PASSWORD" ]; then
|
||
ADMIN_PASSWORD=$(openssl rand -hex 16)
|
||
elif [ -n "$ADMIN_PASSWORD" ]; then
|
||
print_info "Using custom admin password from ADMIN_PASSWORD env var"
|
||
fi
|
||
touch "$CONSOLE_PATH/data/.force_password_update"
|
||
else
|
||
print_info "Update mode: preserving auth database and panel passwords"
|
||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||
fi
|
||
|
||
if ! merge_console_env "$is_fresh"; then
|
||
return 1
|
||
fi
|
||
|
||
local nodejs_admin_password="${ADMIN_PASSWORD:-}"
|
||
if [ -z "$nodejs_admin_password" ] && [ -f "$CONSOLE_PATH/.env" ]; then
|
||
nodejs_admin_password=$(grep -m1 '^DEFAULT_ADMIN_PASSWORD=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
fi
|
||
|
||
# Persist credentials only when explicitly requested (fresh install).
|
||
if [ "$STORE_ADMIN_CREDENTIALS" = "true" ] && [ "$is_fresh" = true ] && [ -n "$nodejs_admin_password" ]; then
|
||
cat > "$CONSOLE_PATH/data/.admin_credentials" << CREDEOF
|
||
Admin Username: admin
|
||
Admin Password: $nodejs_admin_password
|
||
Generated by: BetterDesk installer
|
||
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
CREDEOF
|
||
chmod 600 "$CONSOLE_PATH/data/.admin_credentials"
|
||
fi
|
||
|
||
# Set permissions
|
||
chown -R root:root "$CONSOLE_PATH"
|
||
chmod -R 755 "$CONSOLE_PATH"
|
||
chmod 600 "$CONSOLE_PATH/.env" 2>/dev/null || true
|
||
|
||
CONSOLE_TYPE="nodejs"
|
||
print_success "Node.js Web Console installed"
|
||
}
|
||
|
||
install_binaries() {
|
||
local force_recompile="${1:-false}"
|
||
|
||
print_step "Installing BetterDesk Go Server..."
|
||
|
||
# Ensure architecture is detected
|
||
if [ -z "$ARCH_NAME" ]; then
|
||
detect_architecture
|
||
fi
|
||
|
||
# Safety: stop services before copying (prevents "Text file busy")
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
print_info "Stopping running services before binary installation..."
|
||
graceful_stop_services
|
||
fi
|
||
|
||
mkdir -p "$RUSTDESK_PATH"
|
||
|
||
local go_binary="$GO_SERVER_SOURCE/betterdesk-server"
|
||
local need_compile=false
|
||
|
||
if [ ! -f "$go_binary" ]; then
|
||
need_compile=true
|
||
print_info "Pre-compiled binary not found, compiling from source..."
|
||
elif [ "$force_recompile" = "true" ]; then
|
||
# During UPDATE: check if any .go source file is newer than the binary
|
||
local newest_source
|
||
newest_source=$(find "$GO_SERVER_SOURCE" -name '*.go' -newer "$go_binary" 2>/dev/null | head -1)
|
||
if [ -n "$newest_source" ]; then
|
||
need_compile=true
|
||
print_info "Source code updated since last build, recompiling..."
|
||
else
|
||
print_info "Binary is up-to-date with source code"
|
||
fi
|
||
fi
|
||
|
||
if [ "$need_compile" = true ]; then
|
||
# Ensure Go is installed
|
||
if ! check_go_installed; then
|
||
print_info "Installing Go toolchain..."
|
||
if ! install_golang; then
|
||
print_error "Failed to install Go toolchain"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Compile the Go server
|
||
if ! compile_go_server; then
|
||
print_error "Failed to compile Go server"
|
||
return 1
|
||
fi
|
||
else
|
||
print_info "Using existing Go server binary"
|
||
fi
|
||
|
||
# Verify binary before installation
|
||
if ! verify_binaries; then
|
||
print_error "Aborting installation due to verification failure"
|
||
return 1
|
||
fi
|
||
|
||
# Copy binary
|
||
cp "$go_binary" "$RUSTDESK_PATH/betterdesk-server"
|
||
chmod +x "$RUSTDESK_PATH/betterdesk-server"
|
||
|
||
print_success "BetterDesk Go Server v$VERSION installed"
|
||
print_info "Single binary replaces both hbbs (signal) and hbbr (relay)"
|
||
}
|
||
|
||
# Flask console removed in v2.3.0 - archived to archive/web-flask/
|
||
|
||
install_console() {
|
||
# Always install Node.js console (Flask removed in v2.3.0)
|
||
local console_choice="nodejs"
|
||
|
||
print_info "Installing Node.js web console..."
|
||
|
||
# Check for existing Flask console and migrate
|
||
if [ -d "$CONSOLE_PATH" ]; then
|
||
if [ -f "$CONSOLE_PATH/app.py" ] && ! [ -f "$CONSOLE_PATH/server.js" ]; then
|
||
print_warning "Legacy Flask console detected at $CONSOLE_PATH"
|
||
if [ "$AUTO_MODE" = false ]; then
|
||
if confirm "Migrate from Flask to Node.js?"; then
|
||
migrate_console "flask" "nodejs"
|
||
else
|
||
print_info "Flask is deprecated. Installing Node.js alongside..."
|
||
fi
|
||
else
|
||
print_info "Auto mode: Migrating from Flask to Node.js"
|
||
migrate_console "flask" "nodejs"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
install_nodejs_console
|
||
}
|
||
|
||
migrate_console() {
|
||
local from_type="$1"
|
||
local to_type="$2"
|
||
|
||
print_step "Migrating from $from_type to $to_type..."
|
||
|
||
# Backup existing console
|
||
local backup_path="$BACKUP_DIR/console_${from_type}_$(date +%Y%m%d_%H%M%S)"
|
||
mkdir -p "$backup_path"
|
||
|
||
# Backup user database (auth.db) if exists
|
||
if [ -f "$CONSOLE_PATH/data/auth.db" ]; then
|
||
cp "$CONSOLE_PATH/data/auth.db" "$backup_path/"
|
||
print_info "Backed up user database"
|
||
fi
|
||
|
||
# Backup .env if exists
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
cp "$CONSOLE_PATH/.env" "$backup_path/"
|
||
fi
|
||
|
||
# Stop old console service
|
||
systemctl stop betterdesk 2>/dev/null || true
|
||
|
||
# Remove old console files but preserve data
|
||
rm -rf "$CONSOLE_PATH/venv" 2>/dev/null || true
|
||
rm -rf "$CONSOLE_PATH/node_modules" 2>/dev/null || true
|
||
rm -f "$CONSOLE_PATH/app.py" "$CONSOLE_PATH/server.js" 2>/dev/null || true
|
||
|
||
print_success "Old $from_type console backed up to $backup_path"
|
||
}
|
||
|
||
generate_ssl_certificates() {
|
||
print_step "Generating self-signed TLS certificates..."
|
||
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
|
||
# Skip if certificates already exist
|
||
if [ -f "$ssl_dir/betterdesk.crt" ] && [ -f "$ssl_dir/betterdesk.key" ]; then
|
||
print_info "TLS certificates already exist at $ssl_dir"
|
||
print_info "Skipping certificate generation (use SSL config menu to regenerate)"
|
||
return 0
|
||
fi
|
||
|
||
# Ensure openssl is available
|
||
if ! command -v openssl &>/dev/null; then
|
||
print_warning "openssl not found - skipping TLS certificate generation"
|
||
print_info "Install openssl and use SSL config menu (option C) to generate later"
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p "$ssl_dir"
|
||
|
||
# Detect server IPs for SAN (Subject Alternative Name)
|
||
local server_ip
|
||
server_ip=$(get_public_ip)
|
||
|
||
# Detect LAN IP (first non-loopback IPv4)
|
||
local lan_ip
|
||
lan_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1 2>/dev/null || \
|
||
hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||
|
||
# Build SAN list
|
||
local san_list="IP:$server_ip,IP:127.0.0.1,DNS:localhost"
|
||
|
||
# Add LAN IP if different from public IP
|
||
if [ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && [ "$lan_ip" != "127.0.0.1" ]; then
|
||
san_list="$san_list,IP:$lan_ip"
|
||
fi
|
||
|
||
# Add custom domain if provided via environment variable
|
||
if [ -n "${SSL_DOMAIN:-}" ]; then
|
||
san_list="DNS:$SSL_DOMAIN,$san_list"
|
||
print_info "Adding domain to certificate: $SSL_DOMAIN"
|
||
fi
|
||
|
||
# Determine CN (Common Name) — prefer domain, fallback to public IP
|
||
local cn="${SSL_DOMAIN:-$server_ip}"
|
||
|
||
# Generate certificate with SAN extension (valid for 10 years for self-signed)
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk/C=PL" \
|
||
-addext "subjectAltName=$san_list" \
|
||
2>&1 || {
|
||
print_warning "Certificate generation failed (openssl too old for -addext?)"
|
||
# Fallback without SAN for older openssl
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk/C=PL" \
|
||
2>&1 || {
|
||
print_error "Failed to generate self-signed certificate"
|
||
return 1
|
||
}
|
||
}
|
||
|
||
# Deploy with console-user-readable permissions (#219)
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key"; then
|
||
print_error "Failed to set permissions on self-signed certificate"
|
||
return 1
|
||
fi
|
||
|
||
# Also symlink to console SSL directory for Node.js
|
||
if [ -d "$CONSOLE_PATH" ]; then
|
||
local console_ssl="$CONSOLE_PATH/ssl"
|
||
mkdir -p "$console_ssl"
|
||
ln -sf "$ssl_dir/betterdesk.crt" "$console_ssl/betterdesk.crt" 2>/dev/null || \
|
||
cp -f "$ssl_dir/betterdesk.crt" "$console_ssl/betterdesk.crt"
|
||
ln -sf "$ssl_dir/betterdesk.key" "$console_ssl/betterdesk.key" 2>/dev/null || \
|
||
cp -f "$ssl_dir/betterdesk.key" "$console_ssl/betterdesk.key"
|
||
|
||
# Enable HTTPS in .env so Node.js console (port 5000 + 21121) uses TLS
|
||
local env_file="$CONSOLE_PATH/.env"
|
||
if [ -f "$env_file" ]; then
|
||
sed -i "s|^HTTPS_ENABLED=.*|HTTPS_ENABLED=true|" "$env_file"
|
||
sed -i "s|^SSL_CERT_PATH=.*|SSL_CERT_PATH=$ssl_dir/betterdesk.crt|" "$env_file"
|
||
sed -i "s|^SSL_KEY_PATH=.*|SSL_KEY_PATH=$ssl_dir/betterdesk.key|" "$env_file"
|
||
# Note: Do NOT change internal Go API URLs to https:// here.
|
||
# API TLS breaks RustDesk clients; Node.js only needs the CA for its own HTTPS endpoints.
|
||
if grep -q '^NODE_EXTRA_CA_CERTS=' "$env_file" 2>/dev/null; then
|
||
sed -i "s|^NODE_EXTRA_CA_CERTS=.*|NODE_EXTRA_CA_CERTS=$ssl_dir/betterdesk.crt|" "$env_file"
|
||
else
|
||
echo "NODE_EXTRA_CA_CERTS=$ssl_dir/betterdesk.crt" >> "$env_file"
|
||
fi
|
||
|
||
# Enterprise TLS compatibility: Go API must remain HTTP because
|
||
# RustDesk desktop clients use plain HTTP on signal_port-2.
|
||
if [ "${ENTERPRISE_TLS:-false}" = "true" ]; then
|
||
if grep -q '^ALLOW_SELF_SIGNED_CERTS=' "$env_file" 2>/dev/null; then
|
||
sed -i "s|^ALLOW_SELF_SIGNED_CERTS=.*|ALLOW_SELF_SIGNED_CERTS=true|" "$env_file"
|
||
else
|
||
echo "ALLOW_SELF_SIGNED_CERTS=true" >> "$env_file"
|
||
fi
|
||
sed -i "s|^HBBS_API_URL=https://localhost|HBBS_API_URL=http://localhost|" "$env_file"
|
||
sed -i "s|^BETTERDESK_API_URL=https://localhost|BETTERDESK_API_URL=http://localhost|" "$env_file"
|
||
print_info "Enterprise TLS: Go API stays HTTP for RustDesk client compatibility"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
print_success "Self-signed TLS certificate generated (valid 10 years)"
|
||
print_info "Certificate: $ssl_dir/betterdesk.crt"
|
||
print_info "Private key: $ssl_dir/betterdesk.key"
|
||
print_info "SANs: $san_list"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && print_info "LAN IP included: $lan_ip"
|
||
return 0
|
||
}
|
||
|
||
# Go :21114 + Node :21121 proxy (keeps legacy RustDesk client API URLs working).
|
||
ensure_api_compat_proxy_layout() {
|
||
local go_port="${GO_API_PORT:-21114}"
|
||
local client_port="${CLIENT_API_PORT:-21121}"
|
||
API_PORT="$go_port"
|
||
local go_svc="/etc/systemd/system/betterdesk-server.service"
|
||
|
||
if [ -f "$go_svc" ] && grep -qE '\-api-port[[:space:]]+21121\b' "$go_svc" 2>/dev/null; then
|
||
print_info "Migrating Go -api-port 21121 → $go_port (handlers on Go; clients stay on :$client_port proxy)"
|
||
sed -i "s/-api-port 21121/-api-port ${go_port}/" "$go_svc"
|
||
fi
|
||
|
||
if [ -z "$CONSOLE_PATH" ] || [ ! -f "$CONSOLE_PATH/.env" ]; then
|
||
return 0
|
||
fi
|
||
|
||
local env_file="$CONSOLE_PATH/.env"
|
||
local env_go_port
|
||
env_go_port=$(grep -oP '^HBBS_API_URL=https?://[^:/]+:\K[0-9]+' "$env_file" 2>/dev/null | head -1)
|
||
if [ "$env_go_port" = "21121" ]; then
|
||
print_info "Pointing panel API URLs to Go :$go_port (was :21121)"
|
||
sed -i "s|://localhost:21121/api|://localhost:${go_port}/api|g" "$env_file"
|
||
sed -i "s|://127.0.0.1:21121/api|://127.0.0.1:${go_port}/api|g" "$env_file"
|
||
fi
|
||
|
||
sed -i 's/^API_ENABLED=.*/API_ENABLED=true/' "$env_file" 2>/dev/null || echo "API_ENABLED=true" >> "$env_file"
|
||
sed -i "s/^API_PORT=.*/API_PORT=$client_port/" "$env_file" 2>/dev/null || echo "API_PORT=$client_port" >> "$env_file"
|
||
sed -i 's/^RUSTDESK_API_PROXY=.*/RUSTDESK_API_PROXY=true/' "$env_file" 2>/dev/null || echo "RUSTDESK_API_PROXY=true" >> "$env_file"
|
||
if grep -q '^GO_API_PORT=' "$env_file" 2>/dev/null; then
|
||
sed -i "s/^GO_API_PORT=.*/GO_API_PORT=$go_port/" "$env_file"
|
||
else
|
||
echo "GO_API_PORT=$go_port" >> "$env_file"
|
||
fi
|
||
sed -i "s|^HBBS_API_URL=.*|HBBS_API_URL=http://localhost:${go_port}/api|" "$env_file"
|
||
sed -i "s|^BETTERDESK_API_URL=.*|BETTERDESK_API_URL=http://localhost:${go_port}/api|" "$env_file"
|
||
}
|
||
|
||
# Safe in-place patch of systemd units (TLS API flags, HTTP URLs) without recreating units.
|
||
patch_service_definitions() {
|
||
local changed=0
|
||
local svc console_svc
|
||
for svc in /etc/systemd/system/betterdesk-server.service; do
|
||
[ -f "$svc" ] || continue
|
||
local content new_content backup
|
||
content=$(cat "$svc")
|
||
new_content=$(printf '%s' "$content" \
|
||
| sed -E 's/[[:space:]]-tls-api(=[^[:space:]]*)?//g' \
|
||
| sed -E 's/[[:space:]]-tls-api-port(=[^[:space:]]*)?//g' \
|
||
| sed 's|Environment=HBBS_API_URL=https://localhost|Environment=HBBS_API_URL=http://localhost|g' \
|
||
| sed 's|Environment=BETTERDESK_API_URL=https://localhost|Environment=BETTERDESK_API_URL=http://localhost|g')
|
||
if [ "$new_content" != "$content" ]; then
|
||
backup="${svc}.bak.$(date +%Y%m%d%H%M%S)"
|
||
cp "$svc" "$backup" 2>/dev/null || true
|
||
printf '%s' "$new_content" > "$svc"
|
||
print_info "Patched $(basename "$svc") (removed incompatible TLS API flags)"
|
||
changed=1
|
||
fi
|
||
done
|
||
|
||
console_svc="/etc/systemd/system/betterdesk-console.service"
|
||
if [ -f "$console_svc" ]; then
|
||
local console_user
|
||
console_user=$(ensure_betterdesk_console_user)
|
||
repair_console_service_user_line "$console_user"
|
||
local content new_content backup
|
||
content=$(cat "$console_svc")
|
||
new_content=$(printf '%s' "$content" \
|
||
| sed 's|Environment=HBBS_API_URL=https://localhost|Environment=HBBS_API_URL=http://localhost|g' \
|
||
| sed 's|Environment=BETTERDESK_API_URL=https://localhost|Environment=BETTERDESK_API_URL=http://localhost|g' \
|
||
| sed '/^ExecStartPre=.*linux-ensure-console-user/d')
|
||
if [ "$console_user" != "root" ] && grep -q '^User=root' <<< "$new_content"; then
|
||
new_content=$(printf '%s' "$new_content" | sed "s/^User=root/User=$console_user/")
|
||
print_info "Patched betterdesk-console.service (User=$console_user)"
|
||
changed=1
|
||
fi
|
||
if [ "$new_content" != "$content" ] && grep -q '^ExecStartPre=.*linux-ensure-console-user' <<< "$content"; then
|
||
print_info "Removed unsafe root ExecStartPre permission hook from betterdesk-console.service"
|
||
changed=1
|
||
fi
|
||
if [ "$new_content" != "$content" ]; then
|
||
backup="${console_svc}.bak.$(date +%Y%m%d%H%M%S)"
|
||
cp "$console_svc" "$backup" 2>/dev/null || true
|
||
printf '%s' "$new_content" > "$console_svc"
|
||
[ "$console_user" = "root" ] || print_info "Patched betterdesk-console.service (Go API URLs stay HTTP)"
|
||
changed=1
|
||
fi
|
||
fi
|
||
|
||
if [ "$changed" -eq 1 ]; then
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
print_success "Service definitions patched (custom ExecStart preserved)"
|
||
fi
|
||
|
||
ensure_go_server_signal_ports 2>/dev/null || true
|
||
}
|
||
|
||
# During UPDATE: create missing units; patch existing ones safely (issue #158).
|
||
# Optional: UPDATE_REFRESH_SERVICES=true or second arg "recreate" → full setup_services.
|
||
maybe_update_services() {
|
||
local mode="${1:-default}"
|
||
local need_setup=false
|
||
if [ ! -f /etc/systemd/system/betterdesk-server.service ]; then
|
||
need_setup=true
|
||
fi
|
||
if [ -f "$CONSOLE_PATH/server.js" ] && [ ! -f /etc/systemd/system/betterdesk-console.service ]; then
|
||
need_setup=true
|
||
fi
|
||
if [ "$need_setup" = true ]; then
|
||
print_info "Service units missing — creating systemd services..."
|
||
setup_services
|
||
return
|
||
fi
|
||
|
||
patch_service_definitions
|
||
repair_https_stuck_state yes
|
||
|
||
if [ "$mode" = "recreate" ] || [ "${UPDATE_REFRESH_SERVICES:-false}" = true ]; then
|
||
print_info "Recreating systemd service units from template..."
|
||
setup_services
|
||
return
|
||
fi
|
||
|
||
print_info "Service units present — patched in place (Repair → Repair services for full recreate)"
|
||
}
|
||
|
||
# Repair corrupted User= lines in betterdesk-console.service (#219).
|
||
# Command substitution must never capture repair warnings on stdout.
|
||
repair_console_service_user_line() {
|
||
local want_user="${1:-betterdesk}"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local user_count valid_count
|
||
|
||
[ -f "$svc_file" ] || return 0
|
||
|
||
user_count=$(grep -c '^User=' "$svc_file" 2>/dev/null || echo 0)
|
||
valid_count=$(grep -cE "^User=(root|betterdesk)$" "$svc_file" 2>/dev/null || echo 0)
|
||
|
||
if [ "$user_count" -eq 1 ] && [ "$valid_count" -eq 1 ]; then
|
||
return 0
|
||
fi
|
||
|
||
print_warning "Repairing invalid User= in betterdesk-console.service (#219)"
|
||
sed -i '/^User=/d' "$svc_file"
|
||
sed -i "/^\[Service\]/a User=${want_user}" "$svc_file"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
}
|
||
|
||
# Internal: permissions + optional LE repair (may print to stderr only).
|
||
_sync_betterdesk_console_user_permissions() {
|
||
local svc_user="betterdesk"
|
||
|
||
if ! id "$svc_user" &>/dev/null; then
|
||
useradd -r -s /usr/sbin/nologin -d /var/lib/betterdesk -c "BetterDesk web console" "$svc_user" 2>/dev/null \
|
||
|| print_warning "Could not create system user '$svc_user' — console will stay on root"
|
||
fi
|
||
if ! id "$svc_user" &>/dev/null; then
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p /var/lib/betterdesk "$CONSOLE_PATH/data" "$RUSTDESK_PATH" "$RUSTDESK_PATH/ssl"
|
||
chown -R "$svc_user:$svc_user" "$CONSOLE_PATH" 2>/dev/null || true
|
||
chown root:"$svc_user" "$RUSTDESK_PATH" 2>/dev/null || true
|
||
chmod 2775 "$RUSTDESK_PATH" 2>/dev/null || true
|
||
if [ -d "$RUSTDESK_PATH/ssl" ]; then
|
||
chown root:"$svc_user" "$RUSTDESK_PATH/ssl" 2>/dev/null || true
|
||
chmod 2750 "$RUSTDESK_PATH/ssl" 2>/dev/null || true
|
||
fi
|
||
if [ -f "$RUSTDESK_PATH/.api_key" ]; then
|
||
chown root:"$svc_user" "$RUSTDESK_PATH/.api_key" 2>/dev/null || true
|
||
chmod 640 "$RUSTDESK_PATH/.api_key" 2>/dev/null || true
|
||
fi
|
||
for f in id_ed25519.pub db_v2.sqlite3 db_v2.sqlite3-wal db_v2.sqlite3-shm; do
|
||
if [ -e "$RUSTDESK_PATH/$f" ]; then
|
||
chown root:"$svc_user" "$RUSTDESK_PATH/$f" 2>/dev/null || true
|
||
chmod g+rw "$RUSTDESK_PATH/$f" 2>/dev/null || true
|
||
fi
|
||
done
|
||
for f in ssl/betterdesk.crt ssl/betterdesk.key; do
|
||
if [ -e "$RUSTDESK_PATH/$f" ]; then
|
||
chown root:"$svc_user" "$RUSTDESK_PATH/$f" 2>/dev/null || true
|
||
chmod 640 "$RUSTDESK_PATH/$f" 2>/dev/null || true
|
||
fi
|
||
done
|
||
{ maybe_repair_le_ssl_symlinks || true; } >&2
|
||
return 0
|
||
}
|
||
|
||
# Create a dedicated unprivileged user for the web console (audit H-7).
|
||
# stdout must contain ONLY the username (used in command substitution).
|
||
ensure_betterdesk_console_user() {
|
||
if ! _sync_betterdesk_console_user_permissions; then
|
||
echo "root"
|
||
return
|
||
fi
|
||
echo "betterdesk"
|
||
}
|
||
|
||
setup_services() {
|
||
print_step "Configuring systemd services..."
|
||
|
||
ensure_api_compat_proxy_layout
|
||
|
||
# SAFETY NET: Re-read database config from .env if shell vars are empty.
|
||
# This prevents PostgreSQL → SQLite regression during UPDATE/REPAIR
|
||
# if preserve_database_config() was not called or vars were lost.
|
||
if [ "$USE_POSTGRESQL" != "true" ] && [ -f "$CONSOLE_PATH/.env" ]; then
|
||
local _env_db_type
|
||
_env_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
if [ "$_env_db_type" = "postgres" ]; then
|
||
POSTGRESQL_URI=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
if [ -n "$POSTGRESQL_URI" ]; then
|
||
USE_POSTGRESQL="true"
|
||
print_info "Recovered PostgreSQL config from existing .env"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Get relay server IP according to RELAY_MODE / RELAY_SERVERS
|
||
# (auto = public IP, local = LAN IP, public = forced public, RELAY_SERVERS = fixed)
|
||
# Interactive relay mode selection (skipped in auto mode or when explicitly set)
|
||
if [ "$AUTO_MODE" = false ] && [ -z "$RELAY_SERVERS" ] && [ "${RELAY_MODE:-auto}" = "auto" ]; then
|
||
local _local_ip _public_ip
|
||
_local_ip=$(get_local_ip)
|
||
echo ""
|
||
print_info "Relay server address controls how clients connect for remote sessions."
|
||
echo -e " ${CYAN}1)${NC} Internet / public ${DIM}(auto-detect public IP — default)${NC}"
|
||
echo -e " ${CYAN}2)${NC} LAN only ${DIM}(use this server's local IP: $_local_ip)${NC}"
|
||
echo -e " ${CYAN}3)${NC} Custom address ${DIM}(enter a specific IP or host)${NC}"
|
||
echo -ne " ${CYAN}Select relay mode [1]:${NC} "
|
||
read -r _relay_choice
|
||
case "$_relay_choice" in
|
||
2) RELAY_MODE="local" ;;
|
||
3)
|
||
echo -ne " ${CYAN}Enter relay address (IP or host[:port]):${NC} "
|
||
read -r RELAY_SERVERS
|
||
;;
|
||
*) RELAY_MODE="auto" ;;
|
||
esac
|
||
echo ""
|
||
fi
|
||
|
||
local server_ip
|
||
server_ip=$(resolve_relay_ip)
|
||
|
||
resolve_connection_mode_env
|
||
|
||
print_info "Relay server IP: $server_ip (mode: ${RELAY_SERVERS:+fixed}${RELAY_SERVERS:-$RELAY_MODE})"
|
||
print_info "API Port: $API_PORT"
|
||
|
||
local signal_rate_limit="${SIGNAL_RATE_LIMIT_PER_IP:-20}"
|
||
if ! [[ "$signal_rate_limit" =~ ^[0-9]+$ ]]; then
|
||
print_warning "Invalid SIGNAL_RATE_LIMIT_PER_IP='$signal_rate_limit'; using 20"
|
||
signal_rate_limit="20"
|
||
fi
|
||
print_info "Signal registration rate limit: $signal_rate_limit/min (0 = disabled)"
|
||
|
||
# Build database configuration
|
||
local db_arg=""
|
||
if [ "$USE_POSTGRESQL" = "true" ] && [ -n "$POSTGRESQL_URI" ]; then
|
||
db_arg="-db \"$POSTGRESQL_URI\""
|
||
print_info "Database: PostgreSQL"
|
||
else
|
||
db_arg="-db \"$RUSTDESK_PATH/db_v2.sqlite3\""
|
||
print_info "Database: SQLite"
|
||
fi
|
||
|
||
# Build TLS arguments if certificates exist
|
||
local tls_arg=""
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local tls_is_selfsigned=false
|
||
if [ -f "$ssl_dir/betterdesk.crt" ] && [ -f "$ssl_dir/betterdesk.key" ]; then
|
||
# Check if certificate is self-signed (issuer == subject after stripping prefix)
|
||
local cert_issuer cert_subject
|
||
cert_issuer=$(openssl x509 -in "$ssl_dir/betterdesk.crt" -noout -issuer 2>/dev/null | sed 's/^issuer[= ]*//' || echo "")
|
||
cert_subject=$(openssl x509 -in "$ssl_dir/betterdesk.crt" -noout -subject 2>/dev/null | sed 's/^subject[= ]*//' || echo "")
|
||
if [ -n "$cert_issuer" ] && [ "$cert_issuer" = "$cert_subject" ]; then
|
||
tls_is_selfsigned=true
|
||
elif echo "$cert_subject" | grep -qi "BetterDesk"; then
|
||
tls_is_selfsigned=true
|
||
fi
|
||
|
||
# Enable TLS on signal/relay for client encryption.
|
||
# API port (21121) MUST stay HTTP — RustDesk desktop clients send plain HTTP
|
||
# to the configured API server URL and do not support HTTPS for API endpoints
|
||
# (heartbeat, sysinfo, login, ab). Enabling -tls-api breaks all clients.
|
||
tls_arg="-tls-cert $ssl_dir/betterdesk.crt -tls-key $ssl_dir/betterdesk.key -tls-signal -tls-relay"
|
||
|
||
if [ "$tls_is_selfsigned" = false ]; then
|
||
print_info "TLS: Enabled for signal/relay (proper certificate found, API stays HTTP)"
|
||
else
|
||
print_info "TLS: Enabled for signal/relay (self-signed cert, API stays HTTP)"
|
||
fi
|
||
else
|
||
print_info "TLS: Disabled (no certificate found)"
|
||
fi
|
||
|
||
# BetterDesk Go Server (single binary replacing hbbs+hbbr)
|
||
# Generate shared API key for Node.js ↔ Go server communication (preserve existing)
|
||
local api_key
|
||
if [ -f "$RUSTDESK_PATH/.api_key" ] && [ -s "$RUSTDESK_PATH/.api_key" ]; then
|
||
api_key=$(cat "$RUSTDESK_PATH/.api_key" | tr -d '\n')
|
||
print_info "Using existing API key for console-server communication"
|
||
else
|
||
api_key=$(openssl rand -hex 32)
|
||
echo "$api_key" > "$RUSTDESK_PATH/.api_key"
|
||
chmod 600 "$RUSTDESK_PATH/.api_key"
|
||
print_info "Generated API key for console-server communication"
|
||
fi
|
||
|
||
# Read admin password from install step (for syncing Go server admin)
|
||
# Escape $ → $$ and % → %% for systemd (ExecStart interprets $VAR
|
||
# as env var substitution and %n/%u/etc. as specifiers)
|
||
local init_admin_arg=""
|
||
if [ -n "$ADMIN_PASSWORD" ]; then
|
||
local escaped_admin_pass
|
||
escaped_admin_pass=$(printf '%s' "$ADMIN_PASSWORD" | sed 's/\$/\$\$/g; s/%/%%/g')
|
||
init_admin_arg="-init-admin-pass $escaped_admin_pass"
|
||
fi
|
||
|
||
# Escape $ and % in database URL for systemd (PostgreSQL passwords can contain $ and %)
|
||
local systemd_db_arg="$db_arg"
|
||
systemd_db_arg=$(printf '%s' "$systemd_db_arg" | sed 's/\$/\$\$/g; s/%/%%/g')
|
||
|
||
cat > /etc/systemd/system/betterdesk-server.service << EOF
|
||
[Unit]
|
||
Description=BetterDesk Go Server v$VERSION (Signal + Relay + API)
|
||
Documentation=https://github.com/UNITRONIX/Rustdesk-FreeConsole
|
||
After=network.target postgresql.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
WorkingDirectory=$RUSTDESK_PATH
|
||
EnvironmentFile=-$CONSOLE_PATH/.env
|
||
Environment=AUTH_DB_PATH=$CONSOLE_PATH/data/auth.db
|
||
Environment=MESH_ENABLED=Y
|
||
Environment=SIGNAL_PORT=21116
|
||
Environment=RELAY_PORT=21117
|
||
Environment=GO_API_PORT=${GO_API_PORT:-21114}
|
||
$CONNECTION_MODE_ENV_BLOCK
|
||
ExecStart=$RUSTDESK_PATH/betterdesk-server -mode all -relay-servers $server_ip $systemd_db_arg -key-file $RUSTDESK_PATH/id_ed25519 -api-port $API_PORT -signal-rate-limit-per-ip $signal_rate_limit $init_admin_arg $tls_arg
|
||
Restart=always
|
||
RestartSec=5
|
||
LimitNOFILE=1000000
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
print_success "Created betterdesk-server.service (Go)"
|
||
|
||
# Remove legacy Rust services if they exist
|
||
if [ -f /etc/systemd/system/rustdesksignal.service ]; then
|
||
systemctl stop rustdesksignal 2>/dev/null || true
|
||
systemctl disable rustdesksignal 2>/dev/null || true
|
||
rm -f /etc/systemd/system/rustdesksignal.service
|
||
print_info "Removed legacy rustdesksignal.service"
|
||
fi
|
||
|
||
if [ -f /etc/systemd/system/rustdeskrelay.service ]; then
|
||
systemctl stop rustdeskrelay 2>/dev/null || true
|
||
systemctl disable rustdeskrelay 2>/dev/null || true
|
||
rm -f /etc/systemd/system/rustdeskrelay.service
|
||
print_info "Removed legacy rustdeskrelay.service"
|
||
fi
|
||
|
||
# Remove legacy Flask betterdesk-api.service (deprecated in v2.3.0)
|
||
if [ -f /etc/systemd/system/betterdesk-api.service ]; then
|
||
systemctl stop betterdesk-api 2>/dev/null || true
|
||
systemctl disable betterdesk-api 2>/dev/null || true
|
||
rm -f /etc/systemd/system/betterdesk-api.service
|
||
print_info "Removed legacy betterdesk-api.service (Flask)"
|
||
fi
|
||
|
||
# Remove stale betterdesk-go.service (manual installs, wrong credentials)
|
||
if [ -f /etc/systemd/system/betterdesk-go.service ]; then
|
||
systemctl stop betterdesk-go 2>/dev/null || true
|
||
systemctl disable betterdesk-go 2>/dev/null || true
|
||
rm -f /etc/systemd/system/betterdesk-go.service
|
||
print_info "Removed stale betterdesk-go.service"
|
||
fi
|
||
|
||
# Console service (Web Interface) - Node.js only
|
||
if [ "$CONSOLE_TYPE" = "nodejs" ]; then
|
||
# Build database environment variables
|
||
# Escape $ → $$ for systemd Environment= directives
|
||
local db_env=""
|
||
if [ "$USE_POSTGRESQL" = "true" ] && [ -n "$POSTGRESQL_URI" ]; then
|
||
local escaped_pg_uri
|
||
escaped_pg_uri=$(printf '%s' "$POSTGRESQL_URI" | sed 's/\$/\$\$/g')
|
||
db_env="Environment=DB_TYPE=postgres
|
||
Environment=DATABASE_URL=$escaped_pg_uri"
|
||
else
|
||
db_env="Environment=DB_TYPE=sqlite
|
||
Environment=DB_PATH=$RUSTDESK_PATH/db_v2.sqlite3"
|
||
fi
|
||
|
||
# API port always stays HTTP (RustDesk clients require plain HTTP)
|
||
local api_scheme="http"
|
||
local tls_env=""
|
||
if [ -n "$tls_arg" ]; then
|
||
# Enable HTTPS on Node.js console (admin panel port 5443). The
|
||
# RustDesk Client API port 21121 has its own TLS switch because
|
||
# stock clients cannot trust self-signed certs here.
|
||
local rustdesk_api_tls="auto"
|
||
[ "$tls_is_selfsigned" = true ] && rustdesk_api_tls="false"
|
||
tls_env="Environment=HTTPS_ENABLED=true
|
||
Environment=SSL_CERT_PATH=$ssl_dir/betterdesk.crt
|
||
Environment=SSL_KEY_PATH=$ssl_dir/betterdesk.key
|
||
Environment=RUSTDESK_API_TLS=$rustdesk_api_tls"
|
||
fi
|
||
|
||
# Detect node binary path dynamically (NodeSource, nvm, system, etc.)
|
||
local node_path
|
||
node_path=$(command -v node 2>/dev/null || which node 2>/dev/null || echo "/usr/bin/node")
|
||
if [ ! -x "$node_path" ]; then
|
||
print_warning "Node.js binary not found at $node_path — service may fail to start"
|
||
fi
|
||
|
||
# Preserve panel listen ports from .env (do not reset :80/:443 → :5000/:5443 on recreate) (#219).
|
||
local console_http_port=5000
|
||
local console_https_port=""
|
||
local console_https_port_env=""
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
console_http_port=$(grep -m1 '^PORT=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
|
||
[ -n "$console_http_port" ] || console_http_port=5000
|
||
console_https_port=$(grep -m1 '^HTTPS_PORT=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
|
||
fi
|
||
if [ -n "$tls_env" ] || grep -qiE '^HTTPS_ENABLED=true' "$CONSOLE_PATH/.env" 2>/dev/null; then
|
||
[ -n "$console_https_port" ] || console_https_port=5443
|
||
console_https_port_env="Environment=HTTPS_PORT=${console_https_port}"
|
||
fi
|
||
|
||
local console_user
|
||
console_user=$(ensure_betterdesk_console_user)
|
||
print_info "Web console service user: $console_user"
|
||
|
||
cat > /etc/systemd/system/betterdesk-console.service << EOF
|
||
[Unit]
|
||
Description=BetterDesk Web Console (Node.js)
|
||
Documentation=https://github.com/UNITRONIX/Rustdesk-FreeConsole
|
||
After=network.target betterdesk-server.service postgresql.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$console_user
|
||
WorkingDirectory=$CONSOLE_PATH
|
||
EnvironmentFile=-$CONSOLE_PATH/.env
|
||
ExecStart=$node_path server.js
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=betterdesk-console
|
||
Environment=NODE_ENV=production
|
||
Environment=RUSTDESK_DIR=$RUSTDESK_PATH
|
||
Environment=KEYS_PATH=$RUSTDESK_PATH
|
||
Environment=DATA_DIR=$CONSOLE_PATH/data
|
||
$db_env
|
||
Environment=HBBS_API_URL=$api_scheme://localhost:${GO_API_PORT:-21114}/api
|
||
Environment=BETTERDESK_API_URL=$api_scheme://localhost:${GO_API_PORT:-21114}/api
|
||
Environment=SERVER_BACKEND=betterdesk
|
||
Environment=API_ENABLED=true
|
||
Environment=API_PORT=${CLIENT_API_PORT:-21121}
|
||
Environment=RUSTDESK_API_PROXY=true
|
||
Environment=GO_API_PORT=${GO_API_PORT:-21114}
|
||
Environment=API_HOST=0.0.0.0
|
||
Environment=PORT=${console_http_port}
|
||
${console_https_port_env}
|
||
Environment=HOST=0.0.0.0
|
||
$tls_env
|
||
$([ "$tls_is_selfsigned" = true ] && echo "Environment=NODE_EXTRA_CA_CERTS=$ssl_dir/betterdesk.crt" || true)
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
print_success "Created betterdesk-console.service (Node.js)"
|
||
repair_console_service_user_line "$console_user"
|
||
|
||
# Remove legacy betterdesk.service if exists
|
||
if [ -f /etc/systemd/system/betterdesk.service ]; then
|
||
systemctl stop betterdesk 2>/dev/null || true
|
||
systemctl disable betterdesk 2>/dev/null || true
|
||
rm -f /etc/systemd/system/betterdesk.service
|
||
print_info "Removed legacy betterdesk.service"
|
||
fi
|
||
fi
|
||
|
||
systemctl daemon-reload
|
||
|
||
print_success "Systemd services configured"
|
||
print_info "Services: betterdesk-server, betterdesk-console"
|
||
}
|
||
|
||
run_migrations() {
|
||
print_step "Running database migrations..."
|
||
|
||
if [ -d "$SCRIPT_DIR/migrations" ]; then
|
||
cd "$SCRIPT_DIR/migrations"
|
||
|
||
# Export auto mode flag for migration scripts
|
||
if [ "$AUTO_MODE" = true ]; then
|
||
export BETTERDESK_AUTO=1
|
||
fi
|
||
|
||
for migration in v*.py; do
|
||
if [ -f "$migration" ]; then
|
||
print_info "Migration: $migration"
|
||
# Pass database path as argument
|
||
python3 "$migration" "$DB_PATH" 2>&1 || {
|
||
print_warning "Migration $migration returned non-zero exit code (may already be applied)"
|
||
}
|
||
fi
|
||
done
|
||
|
||
unset BETTERDESK_AUTO
|
||
fi
|
||
|
||
print_success "Migrations completed"
|
||
}
|
||
|
||
create_admin_user() {
|
||
print_step "Creating admin user..."
|
||
|
||
# Node.js console only (Flask removed in v2.3.0)
|
||
if [ ! -f "$CONSOLE_PATH/server.js" ]; then
|
||
print_warning "No Node.js console detected, skipping admin creation"
|
||
return
|
||
fi
|
||
|
||
# Node.js console - admin is created automatically on startup.
|
||
# Prefer in-memory password from installer, then .env fallback.
|
||
local admin_password="${ADMIN_PASSWORD:-}"
|
||
if [ -z "$admin_password" ] && [ -f "$CONSOLE_PATH/.env" ]; then
|
||
admin_password=$(grep -E '^DEFAULT_ADMIN_PASSWORD=' "$CONSOLE_PATH/.env" | head -1 | cut -d= -f2-)
|
||
fi
|
||
|
||
if [ -n "$admin_password" ]; then
|
||
echo ""
|
||
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ PANEL LOGIN CREDENTIALS ║${NC}"
|
||
echo -e "${GREEN}╠════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ Login: ${WHITE}admin${GREEN} ║${NC}"
|
||
echo -e "${GREEN}║ Password: ${WHITE}${admin_password}${GREEN} ║${NC}"
|
||
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
|
||
if [ "$STORE_ADMIN_CREDENTIALS" = "true" ]; then
|
||
cat > "$RUSTDESK_PATH/.admin_credentials" << CREDEOF
|
||
Admin Username: admin
|
||
Admin Password: $admin_password
|
||
Generated by: BetterDesk installer
|
||
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
CREDEOF
|
||
chmod 600 "$RUSTDESK_PATH/.admin_credentials"
|
||
print_info "Credentials saved in: $RUSTDESK_PATH/.admin_credentials"
|
||
else
|
||
print_warning "Credentials are not persisted by default (security hardening)."
|
||
print_info "Set STORE_ADMIN_CREDENTIALS=true to restore legacy behavior."
|
||
fi
|
||
else
|
||
print_warning "No admin password available for display"
|
||
print_info "Use option 6 (Password reset) if needed"
|
||
fi
|
||
}
|
||
|
||
start_services() {
|
||
# Use enhanced start function with health verification
|
||
start_services_with_verification
|
||
}
|
||
|
||
#===============================================================================
|
||
# BetterDesk Minimal Installation (Go server only, no web console)
|
||
#===============================================================================
|
||
|
||
do_install_minimal() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ MINIMAL INSTALLATION (Server Only) ══════════${NC}"
|
||
echo ""
|
||
|
||
print_info "BetterDesk Minimal installs the Go server binary only."
|
||
print_info "No web console, no Node.js, no npm dependencies."
|
||
print_info "Manage via REST API on port ${GO_API_PORT:-21114} or TCP admin console."
|
||
echo ""
|
||
|
||
detect_installation
|
||
|
||
if [ "$INSTALL_STATUS" = "complete" ]; then
|
||
print_warning "BetterDesk is already installed!"
|
||
if [ "$AUTO_MODE" = false ]; then
|
||
if ! confirm "Do you want to reinstall in Minimal mode?"; then
|
||
return
|
||
fi
|
||
fi
|
||
do_backup_silent
|
||
fi
|
||
|
||
# Choose database type (SQLite or PostgreSQL)
|
||
choose_database_type
|
||
|
||
# Stop services if running
|
||
graceful_stop_services
|
||
|
||
# Minimal: no Node.js dependencies needed
|
||
print_step "Checking system dependencies..."
|
||
command -v curl >/dev/null 2>&1 || apt-get install -y curl
|
||
|
||
# Install and configure PostgreSQL if selected
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
install_postgresql || { print_error "PostgreSQL installation failed"; return 1; }
|
||
setup_postgresql_database || { print_error "PostgreSQL setup failed"; return 1; }
|
||
fi
|
||
|
||
detect_architecture
|
||
install_binaries || { print_error "Binary installation failed"; return 1; }
|
||
|
||
# Skip console installation entirely
|
||
print_info "Skipping web console (Minimal mode)"
|
||
|
||
# Generate self-signed TLS certificates (default for fresh installs)
|
||
generate_ssl_certificates
|
||
|
||
# Migrate existing SQLite data to PostgreSQL if applicable
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
migrate_sqlite_to_postgresql
|
||
fi
|
||
|
||
# Setup only the Go server service (no console service)
|
||
setup_services_minimal
|
||
|
||
# Configure firewall rules (signal + relay + API only, no console ports)
|
||
print_step "Configuring firewall rules..."
|
||
if command -v ufw >/dev/null 2>&1; then
|
||
ufw allow "${GO_API_PORT:-21114}/tcp" comment "BetterDesk Go API (default)" 2>/dev/null || true
|
||
ufw allow 21115/tcp comment "BetterDesk NAT" 2>/dev/null || true
|
||
ufw allow 21116/tcp comment "BetterDesk Signal TCP" 2>/dev/null || true
|
||
ufw allow 21116/udp comment "BetterDesk Signal UDP" 2>/dev/null || true
|
||
ufw allow 21117/tcp comment "BetterDesk Relay" 2>/dev/null || true
|
||
ufw allow 21118/tcp comment "BetterDesk WS Signal" 2>/dev/null || true
|
||
ufw allow 21119/tcp comment "BetterDesk WS Relay" 2>/dev/null || true
|
||
fi
|
||
|
||
# Start server
|
||
print_step "Starting BetterDesk server..."
|
||
systemctl daemon-reload
|
||
systemctl start betterdesk-server.service 2>/dev/null || true
|
||
systemctl enable betterdesk-server.service 2>/dev/null || true
|
||
|
||
sleep 3
|
||
|
||
# Verify
|
||
if systemctl is-active --quiet betterdesk-server.service; then
|
||
print_success "BetterDesk server is running"
|
||
else
|
||
print_error "BetterDesk server failed to start"
|
||
journalctl -u betterdesk-server.service --no-pager -n 20
|
||
return 1
|
||
fi
|
||
|
||
echo ""
|
||
print_success "===== BETTERDESK MINIMAL INSTALLATION COMPLETE ====="
|
||
echo ""
|
||
|
||
local SERVER_IP
|
||
SERVER_IP=$(get_public_ip)
|
||
|
||
echo -e "${GREEN}Server: ${SERVER_IP}${NC}"
|
||
echo -e "${GREEN}Go API: http://${SERVER_IP}:${GO_API_PORT:-21114}${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}Ports: ${GO_API_PORT:-21114} (Go API), ${CLIENT_API_PORT:-21121} (compat proxy, full install), 21115-21117 (Signal/Relay)${NC}"
|
||
echo -e "${YELLOW}No web console installed. Use REST API or TCP admin for management.${NC}"
|
||
echo ""
|
||
|
||
press_enter
|
||
}
|
||
|
||
setup_services_minimal() {
|
||
print_step "Setting up BetterDesk server service (Minimal mode)..."
|
||
|
||
local GO_BINARY_PATH="$INSTALL_DIR/betterdesk-server"
|
||
local KEY_DIR="$INSTALL_DIR"
|
||
local DB_DIR="$INSTALL_DIR"
|
||
|
||
# Build server arguments
|
||
local SERVER_ARGS="-key $KEY_DIR"
|
||
SERVER_ARGS="$SERVER_ARGS -db $DB_DIR"
|
||
|
||
# Add relay servers argument
|
||
local SERVER_IP
|
||
SERVER_IP=$(resolve_relay_ip)
|
||
if [ -n "$SERVER_IP" ]; then
|
||
SERVER_ARGS="$SERVER_ARGS -relay-servers $SERVER_IP"
|
||
fi
|
||
resolve_connection_mode_env
|
||
|
||
local signal_rate_limit="${SIGNAL_RATE_LIMIT_PER_IP:-20}"
|
||
if ! [[ "$signal_rate_limit" =~ ^[0-9]+$ ]]; then
|
||
print_warning "Invalid SIGNAL_RATE_LIMIT_PER_IP='$signal_rate_limit'; using 20"
|
||
signal_rate_limit="20"
|
||
fi
|
||
SERVER_ARGS="$SERVER_ARGS -signal-rate-limit-per-ip $signal_rate_limit"
|
||
|
||
# Database configuration for Go server
|
||
# Escape $ -> $$ for systemd (PostgreSQL passwords can contain $)
|
||
local GO_ENV=""
|
||
if [ "$USE_POSTGRESQL" = "true" ] && [ -n "$POSTGRESQL_URI" ]; then
|
||
local escaped_pg_uri
|
||
escaped_pg_uri=$(printf '%s' "$POSTGRESQL_URI" | sed 's/\$/\$\$/g')
|
||
GO_ENV="Environment=\"DB_URL=$escaped_pg_uri\""
|
||
fi
|
||
|
||
# TLS configuration — look for certificates in standard ssl/ directory
|
||
local SSL_DIR="$INSTALL_DIR/ssl"
|
||
local TLS_CERT_PATH="$SSL_DIR/betterdesk.crt"
|
||
local TLS_KEY_PATH="$SSL_DIR/betterdesk.key"
|
||
|
||
# Also check legacy paths for backwards compatibility
|
||
if [ ! -f "$TLS_CERT_PATH" ] && [ -f "$INSTALL_DIR/cert.pem" ]; then
|
||
TLS_CERT_PATH="$INSTALL_DIR/cert.pem"
|
||
TLS_KEY_PATH="$INSTALL_DIR/key.pem"
|
||
fi
|
||
|
||
if [ -f "$TLS_CERT_PATH" ] && [ -f "$TLS_KEY_PATH" ]; then
|
||
SERVER_ARGS="$SERVER_ARGS -tls-cert $TLS_CERT_PATH -tls-key $TLS_KEY_PATH -tls-signal -tls-relay"
|
||
|
||
# Enterprise TLS still keeps the Go API HTTP for RustDesk client
|
||
# compatibility. Only signal/relay receive TLS flags here.
|
||
if [ "${ENTERPRISE_TLS:-false}" = "true" ]; then
|
||
print_info "Enterprise TLS enabled: API port ${API_PORT:-21121} stays HTTP"
|
||
fi
|
||
fi
|
||
|
||
# Remove old services (cleanup)
|
||
for old_svc in rustdesksignal rustdeskrelay betterdesk-api betterdesk-go betterdesk-console; do
|
||
if systemctl is-active --quiet "$old_svc.service" 2>/dev/null; then
|
||
systemctl stop "$old_svc.service" 2>/dev/null || true
|
||
fi
|
||
if [ -f "/etc/systemd/system/$old_svc.service" ]; then
|
||
systemctl disable "$old_svc.service" 2>/dev/null || true
|
||
rm -f "/etc/systemd/system/$old_svc.service"
|
||
fi
|
||
done
|
||
|
||
cat > /etc/systemd/system/betterdesk-server.service <<EOF
|
||
[Unit]
|
||
Description=BetterDesk Server (Minimal)
|
||
After=network.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
WorkingDirectory=$INSTALL_DIR
|
||
$( [ -n "$CONSOLE_PATH" ] && [ -f "$CONSOLE_PATH/.env" ] && echo "EnvironmentFile=-$CONSOLE_PATH/.env" )
|
||
ExecStart=$GO_BINARY_PATH $SERVER_ARGS
|
||
Restart=always
|
||
RestartSec=5
|
||
$GO_ENV
|
||
Environment=MESH_ENABLED=Y
|
||
$CONNECTION_MODE_ENV_BLOCK
|
||
|
||
# Hardening
|
||
NoNewPrivileges=true
|
||
ProtectSystem=strict
|
||
ReadWritePaths=$INSTALL_DIR $DB_DIR
|
||
ProtectHome=true
|
||
PrivateTmp=true
|
||
|
||
# Logging
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=betterdesk-server
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
print_success "BetterDesk server service created (Minimal mode)"
|
||
}
|
||
|
||
do_install() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ FRESH INSTALLATION ══════════${NC}"
|
||
echo ""
|
||
|
||
detect_installation
|
||
|
||
if [ "$INSTALL_STATUS" = "complete" ]; then
|
||
print_warning "BetterDesk is already installed!"
|
||
if [ "$AUTO_MODE" = false ]; then
|
||
if ! confirm "Do you want to reinstall?"; then
|
||
return
|
||
fi
|
||
fi
|
||
do_backup_silent
|
||
fi
|
||
|
||
echo ""
|
||
print_info "Starting BetterDesk Console v$VERSION installation..."
|
||
echo ""
|
||
|
||
# Choose database type (SQLite or PostgreSQL)
|
||
choose_database_type
|
||
|
||
# Stop services if running (prevents "Text file busy" error)
|
||
graceful_stop_services
|
||
|
||
install_dependencies
|
||
|
||
# Install and configure PostgreSQL if selected
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
install_postgresql || { print_error "PostgreSQL installation failed"; return 1; }
|
||
setup_postgresql_database || { print_error "PostgreSQL setup failed"; return 1; }
|
||
fi
|
||
|
||
detect_architecture
|
||
install_binaries || { print_error "Binary installation failed"; return 1; }
|
||
install_console
|
||
|
||
# Generate self-signed TLS certificates (default for fresh installs)
|
||
generate_ssl_certificates
|
||
|
||
# Migrate existing SQLite data to PostgreSQL if applicable
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
migrate_sqlite_to_postgresql
|
||
fi
|
||
|
||
setup_services
|
||
run_migrations
|
||
create_admin_user
|
||
|
||
# Configure firewall rules
|
||
print_step "Configuring firewall rules..."
|
||
configure_firewall_rules
|
||
|
||
start_services
|
||
|
||
# Post-install verification: confirm services are actually running
|
||
local install_ok=true
|
||
sleep 2
|
||
|
||
local go_state
|
||
go_state=$(systemctl show betterdesk-server --property=ActiveState --value 2>/dev/null || echo "unknown")
|
||
if [ "$go_state" != "active" ]; then
|
||
print_error "betterdesk-server is $go_state (expected: active)"
|
||
print_info "Debug: journalctl -u betterdesk-server -n 30 --no-pager"
|
||
install_ok=false
|
||
fi
|
||
|
||
local console_state
|
||
console_state=$(systemctl show betterdesk-console --property=ActiveState --value 2>/dev/null || echo "unknown")
|
||
if [ "$console_state" != "active" ]; then
|
||
print_warning "betterdesk-console is $console_state (expected: active)"
|
||
print_info "Debug: journalctl -u betterdesk-console -n 30 --no-pager"
|
||
install_ok=false
|
||
fi
|
||
|
||
echo ""
|
||
if [ "$install_ok" = true ]; then
|
||
print_success "Installation completed successfully!"
|
||
else
|
||
print_warning "Installation finished but some services are not running."
|
||
print_info "Run option 8 (Diagnostics) to investigate."
|
||
fi
|
||
echo ""
|
||
|
||
local server_ip
|
||
server_ip=$(get_public_ip)
|
||
local public_key=""
|
||
if [ -f "$RUSTDESK_PATH/id_ed25519.pub" ]; then
|
||
public_key=$(cat "$RUSTDESK_PATH/id_ed25519.pub")
|
||
fi
|
||
|
||
local db_type_info="SQLite"
|
||
if [ "$USE_POSTGRESQL" = "true" ]; then
|
||
db_type_info="PostgreSQL"
|
||
fi
|
||
|
||
local tls_status="Disabled"
|
||
if [ -f "$RUSTDESK_PATH/ssl/betterdesk.crt" ] && [ -f "$RUSTDESK_PATH/ssl/betterdesk.key" ]; then
|
||
tls_status="Self-signed (auto-generated)"
|
||
fi
|
||
|
||
echo -e "${CYAN}╔════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${CYAN}║ INSTALLATION INFO ║${NC}"
|
||
echo -e "${CYAN}╠════════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${CYAN}║ Panel Web: ${WHITE}http://$server_ip:5000${CYAN} ║${NC}"
|
||
echo -e "${CYAN}║ API Port: ${WHITE}$API_PORT${CYAN} ║${NC}"
|
||
echo -e "${CYAN}║ Server ID: ${WHITE}$server_ip${CYAN} ║${NC}"
|
||
echo -e "${CYAN}║ Database: ${WHITE}$db_type_info${CYAN} ║${NC}"
|
||
echo -e "${CYAN}║ TLS: ${WHITE}$tls_status${CYAN} ║${NC}"
|
||
echo -e "${CYAN}║ Key: ${WHITE}${public_key:0:20}...${CYAN} ║${NC}"
|
||
echo -e "${CYAN}╚════════════════════════════════════════════════════════════╝${NC}"
|
||
|
||
# Offer TLS configuration for fresh installs
|
||
if [ "$install_ok" = true ] && [ "$AUTO_MODE" = false ]; then
|
||
echo ""
|
||
print_info "Production TLS options:"
|
||
print_info " • External reverse proxy (Caddy/Nginx on :443) — recommended when a proxy already handles certificates"
|
||
print_info " • Enterprise TLS (Option 5 in SSL menu) — BetterDesk-native HTTPS on panel + signal/relay"
|
||
echo ""
|
||
if confirm "Will TLS terminate at an external reverse proxy (Caddy/Nginx)?"; then
|
||
do_configure_reverse_proxy || true
|
||
elif confirm "Would you like to configure HTTPS Enterprise now? (Option 5 in SSL menu)"; then
|
||
do_configure_ssl
|
||
fi
|
||
fi
|
||
|
||
if [ "$AUTO_MODE" = false ]; then
|
||
press_enter
|
||
fi
|
||
}
|
||
|
||
#===============================================================================
|
||
# Update Functions
|
||
#===============================================================================
|
||
|
||
# GitHub repository configuration for online updates
|
||
UPDATE_GITHUB_OWNER="${UPDATE_GITHUB_OWNER:-UNITRONIX}"
|
||
UPDATE_GITHUB_REPO="${UPDATE_GITHUB_REPO:-BetterDesk}"
|
||
UPDATE_GITHUB_BRANCH="${UPDATE_GITHUB_BRANCH:-main}"
|
||
UPDATE_CLONE_DIR="/tmp/betterdesk-update-$$"
|
||
|
||
read_update_github_branch_from_env() {
|
||
local env_file="${CONSOLE_PATH:-}/.env"
|
||
if [ -n "$CONSOLE_PATH" ] && [ -f "$env_file" ]; then
|
||
local val
|
||
val=$(grep -E '^UPDATE_GITHUB_BRANCH=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '\r"')
|
||
if [ -n "$val" ]; then
|
||
UPDATE_GITHUB_BRANCH="$val"
|
||
export UPDATE_GITHUB_BRANCH
|
||
fi
|
||
fi
|
||
}
|
||
|
||
resolve_update_remote_sha() {
|
||
local clone_dir="$1"
|
||
local remote_sha=""
|
||
|
||
if command -v git &>/dev/null && [ -d "$clone_dir/.git" ]; then
|
||
remote_sha=$(git -C "$clone_dir" rev-parse HEAD 2>/dev/null || true)
|
||
fi
|
||
if ! [[ "$remote_sha" =~ ^[0-9a-fA-F]{40}$ ]] && command -v git &>/dev/null; then
|
||
remote_sha=$(git ls-remote \
|
||
"https://github.com/${UPDATE_GITHUB_OWNER}/${UPDATE_GITHUB_REPO}.git" \
|
||
"refs/heads/${UPDATE_GITHUB_BRANCH}" 2>/dev/null | awk 'NR == 1 { print $1; exit }' || true)
|
||
fi
|
||
if ! [[ "$remote_sha" =~ ^[0-9a-fA-F]{40}$ ]] && command -v curl &>/dev/null; then
|
||
remote_sha=$(curl -fsSL --connect-timeout 15 --max-time 30 \
|
||
-H "Accept: application/vnd.github+json" \
|
||
"https://api.github.com/repos/${UPDATE_GITHUB_OWNER}/${UPDATE_GITHUB_REPO}/commits?sha=${UPDATE_GITHUB_BRANCH}&per_page=1" \
|
||
2>/dev/null | awk -F'"' '/"sha"[[:space:]]*:/ { print $4; exit }' || true)
|
||
fi
|
||
|
||
if [[ "$remote_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
||
printf '%s\n' "$remote_sha"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
write_update_github_branch_to_env() {
|
||
local branch="$1"
|
||
local env_file="${CONSOLE_PATH:-}/.env"
|
||
if [ -z "$CONSOLE_PATH" ]; then
|
||
print_error "Console path unknown — cannot save update channel"
|
||
return 1
|
||
fi
|
||
if [ ! -f "$env_file" ]; then
|
||
touch "$env_file"
|
||
fi
|
||
if grep -qE '^UPDATE_GITHUB_BRANCH=' "$env_file" 2>/dev/null; then
|
||
sed -i "s/^UPDATE_GITHUB_BRANCH=.*/UPDATE_GITHUB_BRANCH=${branch}/" "$env_file"
|
||
else
|
||
printf '\nUPDATE_GITHUB_BRANCH=%s\n' "$branch" >> "$env_file"
|
||
fi
|
||
UPDATE_GITHUB_BRANCH="$branch"
|
||
export UPDATE_GITHUB_BRANCH
|
||
print_success "Update channel saved (GitHub branch: $branch)"
|
||
return 0
|
||
}
|
||
|
||
switch_update_channel() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ UPDATE CHANNEL ══════════${NC}"
|
||
echo ""
|
||
detect_installation
|
||
if [ "$INSTALL_STATUS" = "none" ]; then
|
||
print_error "BetterDesk is not installed!"
|
||
press_enter
|
||
return
|
||
fi
|
||
read_update_github_branch_from_env
|
||
print_info "Current GitHub branch: $UPDATE_GITHUB_BRANCH"
|
||
echo ""
|
||
local _menu_items=(
|
||
$'Stable (main)\tProduction releases from the main branch'
|
||
$'Development (dev)\tLatest work-in-progress from the dev branch'
|
||
$'Back\tReturn without changes'
|
||
)
|
||
local _menu_returns=( main dev 0 )
|
||
menu_choose "Update Channel" "Stable is recommended for production servers"
|
||
case "${MENU_CHOICE:-main}" in
|
||
0) return ;;
|
||
main)
|
||
write_update_github_branch_to_env "main"
|
||
;;
|
||
dev)
|
||
print_warning "Development channel may include unstable changes."
|
||
write_update_github_branch_to_env "dev"
|
||
;;
|
||
*)
|
||
write_update_github_branch_to_env "main"
|
||
;;
|
||
esac
|
||
print_info "Run 'Check for updates' in the console or use Online GitHub update to apply."
|
||
press_enter
|
||
}
|
||
|
||
run_terminal_project_update() {
|
||
local cli_path="$CONSOLE_PATH/scripts/update-cli.js"
|
||
local node_bin=""
|
||
node_bin=$(command -v node 2>/dev/null || true)
|
||
|
||
if [ -z "$node_bin" ] || [ ! -f "$cli_path" ]; then
|
||
return 2
|
||
fi
|
||
|
||
print_step "Running commit-aware project updater..."
|
||
print_info "Updater CLI: $cli_path"
|
||
|
||
local args=()
|
||
if [ "${AUTO_MODE:-false}" = "true" ]; then
|
||
args+=("--yes")
|
||
fi
|
||
|
||
"$node_bin" "$cli_path" "${args[@]}"
|
||
return $?
|
||
}
|
||
|
||
# Pull latest project from GitHub and apply update to local installation.
|
||
# This is the primary update path — it fetches the full repo, rebuilds
|
||
# the Go server, and reinstalls the Node.js console from fresh source.
|
||
# All local state (databases, keys, .env, auth.db) is preserved.
|
||
update_from_github() {
|
||
local clone_dir="$UPDATE_CLONE_DIR"
|
||
local server_build_failed=0
|
||
local previous_source_dir=""
|
||
local remote_sha=""
|
||
|
||
read_update_github_branch_from_env
|
||
|
||
# Clean up any leftover clone from a previous failed run
|
||
rm -rf "$clone_dir"
|
||
|
||
# ---- Step 1: Clone or download latest code ----
|
||
print_step "Downloading latest BetterDesk from GitHub..."
|
||
if command -v git &>/dev/null; then
|
||
local repo_url="https://github.com/${UPDATE_GITHUB_OWNER}/${UPDATE_GITHUB_REPO}.git"
|
||
if ! git clone --depth 1 --single-branch --branch "$UPDATE_GITHUB_BRANCH" "$repo_url" "$clone_dir" 2>/dev/null; then
|
||
print_error "git clone failed"
|
||
rm -rf "$clone_dir"
|
||
return 1
|
||
fi
|
||
print_success "Repository cloned (branch: $UPDATE_GITHUB_BRANCH)"
|
||
else
|
||
# Fallback: download tarball via curl
|
||
local tarball_url="https://github.com/${UPDATE_GITHUB_OWNER}/${UPDATE_GITHUB_REPO}/archive/refs/heads/${UPDATE_GITHUB_BRANCH}.tar.gz"
|
||
local tarball_path="/tmp/betterdesk-update-$$.tar.gz"
|
||
print_info "git not available, downloading tarball..."
|
||
if ! curl -fsSL --connect-timeout 15 --max-time 120 -o "$tarball_path" "$tarball_url"; then
|
||
print_error "Download failed. Check internet connection."
|
||
rm -f "$tarball_path"
|
||
return 1
|
||
fi
|
||
mkdir -p "$clone_dir"
|
||
if ! tar -xzf "$tarball_path" -C "$clone_dir" --strip-components=1; then
|
||
print_error "Failed to extract update archive"
|
||
rm -f "$tarball_path" && rm -rf "$clone_dir"
|
||
return 1
|
||
fi
|
||
rm -f "$tarball_path"
|
||
print_success "Source downloaded and extracted"
|
||
fi
|
||
|
||
# Validate downloaded source
|
||
if [ ! -f "$clone_dir/betterdesk-server/go.mod" ] || [ ! -f "$clone_dir/web-nodejs/server.js" ]; then
|
||
print_error "Downloaded source is incomplete or invalid"
|
||
rm -rf "$clone_dir"
|
||
return 1
|
||
fi
|
||
|
||
if ! remote_sha=$(resolve_update_remote_sha "$clone_dir"); then
|
||
print_error "Could not resolve the downloaded commit SHA; refusing an untracked update"
|
||
rm -rf "$clone_dir"
|
||
return 1
|
||
fi
|
||
print_info "Downloaded commit: ${remote_sha:0:7}"
|
||
|
||
# Read remote version
|
||
local remote_version=""
|
||
if [ -f "$clone_dir/VERSION" ]; then
|
||
remote_version=$(cat "$clone_dir/VERSION" | tr -d '[:space:]')
|
||
fi
|
||
if [ -n "$remote_version" ]; then
|
||
print_info "Remote version: $remote_version"
|
||
fi
|
||
|
||
# ---- Step 2: Update Go server source & compile ----
|
||
print_step "Updating Go server source..."
|
||
if [ -d "$GO_SERVER_SOURCE" ]; then
|
||
# Keep the old tree until the new server has built successfully so a
|
||
# failed update can restore a known-good source tree.
|
||
previous_source_dir="${GO_SERVER_SOURCE}.pre-update.$$"
|
||
if ! mv "$GO_SERVER_SOURCE" "$previous_source_dir" 2>/dev/null; then
|
||
previous_source_dir=""
|
||
print_warning "Could not stage the previous Go source tree; update will continue in place"
|
||
fi
|
||
fi
|
||
# Copy the *contents* into a guaranteed-existing destination. Copying the
|
||
# directory itself would nest the new tree inside an existing
|
||
# $GO_SERVER_SOURCE if the rename above failed (e.g. a locked/busy file),
|
||
# leaving the old inconsistent source in place and breaking `go build`
|
||
# with "undefined" errors (issue #158).
|
||
mkdir -p "$GO_SERVER_SOURCE"
|
||
cp -rf "$clone_dir/betterdesk-server/." "$GO_SERVER_SOURCE/"
|
||
|
||
# Restore any local data/ directory that existed in the old source dir
|
||
if [ -n "$previous_source_dir" ] && [ -d "$previous_source_dir/data" ]; then
|
||
cp -rn "$previous_source_dir/data" "$GO_SERVER_SOURCE/" 2>/dev/null || true
|
||
fi
|
||
print_success "Go server source updated"
|
||
|
||
# Compile Go server
|
||
print_step "Building Go server..."
|
||
if ! check_go_installed; then
|
||
print_info "Installing Go toolchain..."
|
||
if ! install_golang; then
|
||
print_warning "Go toolchain not available — server binary not updated"
|
||
print_info "Install Go manually from https://go.dev/dl/ and re-run update"
|
||
server_build_failed=1
|
||
fi
|
||
fi
|
||
|
||
if check_go_installed; then
|
||
if compile_go_server; then
|
||
print_success "Go server compiled successfully"
|
||
# Deploy binary to installation path
|
||
if [ -f "$GO_SERVER_SOURCE/betterdesk-server" ]; then
|
||
# Backup existing binary
|
||
if [ -f "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
cp "$RUSTDESK_PATH/betterdesk-server" \
|
||
"$RUSTDESK_PATH/betterdesk-server.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
fi
|
||
kill_stale_processes "betterdesk-server"
|
||
kill_processes_holding_ports
|
||
cp "$GO_SERVER_SOURCE/betterdesk-server" "$RUSTDESK_PATH/betterdesk-server"
|
||
chmod +x "$RUSTDESK_PATH/betterdesk-server"
|
||
print_success "Go server binary deployed to $RUSTDESK_PATH"
|
||
fi
|
||
else
|
||
print_warning "Go server compilation failed — keeping existing binary"
|
||
print_info "Use the panel Rebuild server binary button or option 7 (Build & deploy server)"
|
||
server_build_failed=1
|
||
fi
|
||
else
|
||
server_build_failed=1
|
||
fi
|
||
|
||
# ---- Step 3: Update Node.js console files ----
|
||
print_step "Updating Node.js web console..."
|
||
|
||
# Preserve critical local state files before overwriting
|
||
local state_files=(".env" "data" "node_modules")
|
||
local preserved_dir="/tmp/betterdesk-console-state-$$"
|
||
mkdir -p "$preserved_dir"
|
||
|
||
for item in "${state_files[@]}"; do
|
||
if [ -e "$CONSOLE_PATH/$item" ]; then
|
||
cp -a "$CONSOLE_PATH/$item" "$preserved_dir/$item" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# Copy new console files (overwrite code, but not state)
|
||
# Use rsync if available for selective copy, otherwise cp
|
||
if command -v rsync &>/dev/null; then
|
||
rsync -a --delete \
|
||
--exclude='data/' \
|
||
--exclude='node_modules/' \
|
||
--exclude='.env' \
|
||
--exclude='.env.local' \
|
||
--exclude='*.sqlite3' \
|
||
--exclude='*.sqlite3-wal' \
|
||
--exclude='*.sqlite3-shm' \
|
||
--exclude='*.db' \
|
||
--exclude='*.db-wal' \
|
||
--exclude='*.db-shm' \
|
||
--exclude='.session_secret' \
|
||
--exclude='.update_sha' \
|
||
--exclude='.api_key' \
|
||
--exclude='.admin_credentials' \
|
||
--exclude='.force_password_update' \
|
||
"$clone_dir/web-nodejs/" "$CONSOLE_PATH/"
|
||
else
|
||
# cp fallback: copy everything then restore state (glob * skips dotfiles — #166)
|
||
cp -r "$clone_dir/web-nodejs/"* "$CONSOLE_PATH/"
|
||
if [ -f "$clone_dir/web-nodejs/.env.example" ]; then
|
||
cp -a "$clone_dir/web-nodejs/.env.example" "$CONSOLE_PATH/.env.example"
|
||
fi
|
||
# Restore preserved state files
|
||
for item in "${state_files[@]}"; do
|
||
if [ -e "$preserved_dir/$item" ]; then
|
||
if [ -d "$preserved_dir/$item" ]; then
|
||
# For directories (data/, node_modules/), don't delete the new
|
||
# copy — just ensure old files are restored
|
||
cp -a "$preserved_dir/$item/"* "$CONSOLE_PATH/$item/" 2>/dev/null || true
|
||
else
|
||
cp -a "$preserved_dir/$item" "$CONSOLE_PATH/$item" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
rm -rf "$preserved_dir"
|
||
print_success "Console files updated"
|
||
|
||
# Install npm dependencies if package.json changed
|
||
print_step "Installing npm dependencies..."
|
||
cd "$CONSOLE_PATH"
|
||
local npm_log="/tmp/betterdesk_npm_install.log"
|
||
if npm install --production --no-audit --no-fund > "$npm_log" 2>&1; then
|
||
print_success "npm dependencies installed"
|
||
else
|
||
print_warning "npm install had issues (non-critical):"
|
||
tail -5 "$npm_log"
|
||
fi
|
||
rm -f "$npm_log"
|
||
|
||
# Stage Go support-agent source for the Generator build worker
|
||
print_step "Staging support-agent source for Generator builds..."
|
||
if stage_support_agent_source "$clone_dir"; then
|
||
mkdir -p "$CONSOLE_PATH/data"
|
||
printf '{"reason":"betterdesk.sh update","at":"%s"}\n' \
|
||
"$(date -Iseconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||
> "$CONSOLE_PATH/data/.agent_rebuild_pending"
|
||
print_info "Generator bundles will rebuild after console restart"
|
||
else
|
||
print_warning "Support-agent source staging skipped"
|
||
fi
|
||
|
||
# Merge any new .env keys from .env.example (preserve operator settings — issue #158)
|
||
print_step "Merging new .env configuration keys..."
|
||
merge_console_env false || print_warning ".env merge skipped (merge-env.js unavailable)"
|
||
|
||
# ---- Step 4: Update installer scripts ----
|
||
print_step "Updating installer scripts..."
|
||
local scripts_updated=0
|
||
for script_file in install.sh betterdesk.sh betterdesk.ps1 betterdesk-docker.sh \
|
||
docker-compose.yml docker-compose.single.yml docker-compose.quick.yml \
|
||
docker-compose.quick.single.yml docker-compose.quick.single.macvlan.yml \
|
||
Dockerfile Dockerfile.server Dockerfile.console docker-entrypoint.sh \
|
||
docker/entrypoint.sh docker/server-entrypoint.sh docker/console-entrypoint.sh \
|
||
docker/supervisord.conf scripts/installer-protocol-check.js VERSION; do
|
||
if [ -f "$clone_dir/$script_file" ]; then
|
||
cp "$clone_dir/$script_file" "$SCRIPT_DIR/$script_file" 2>/dev/null || true
|
||
if [[ "$script_file" == *.sh ]]; then
|
||
chmod +x "$SCRIPT_DIR/$script_file" 2>/dev/null || true
|
||
fi
|
||
scripts_updated=$((scripts_updated + 1))
|
||
fi
|
||
done
|
||
print_success "$scripts_updated installer files updated"
|
||
|
||
if [ "$server_build_failed" -eq 1 ]; then
|
||
if [ -n "$previous_source_dir" ] && [ -d "$previous_source_dir" ]; then
|
||
rm -rf "$GO_SERVER_SOURCE"
|
||
mv "$previous_source_dir" "$GO_SERVER_SOURCE" 2>/dev/null || \
|
||
print_warning "Could not restore the previous Go source tree"
|
||
fi
|
||
rm -rf "$clone_dir"
|
||
print_error "Go server binary was not rebuilt — update incomplete for server component"
|
||
return 1
|
||
fi
|
||
|
||
# Only mark the update complete after the server build/deploy succeeded.
|
||
mkdir -p "$CONSOLE_PATH/data"
|
||
printf '%s\n' "$remote_sha" > "$CONSOLE_PATH/data/.update_sha"
|
||
printf '%s\n' "$remote_sha" > "$CONSOLE_PATH/data/.agent_source_sha"
|
||
rm -f "$CONSOLE_PATH/data/.last_update_result.json"
|
||
print_info "SHA tracking updated: ${remote_sha:0:7}"
|
||
|
||
if [ -f "$clone_dir/VERSION" ] && [ -n "$remote_version" ]; then
|
||
cp "$clone_dir/VERSION" "$SCRIPT_DIR/VERSION" 2>/dev/null || true
|
||
cp "$clone_dir/VERSION" "$CONSOLE_PATH/VERSION" 2>/dev/null || true
|
||
fi
|
||
rm -rf "$clone_dir"
|
||
if [ -n "$previous_source_dir" ]; then
|
||
rm -rf "$previous_source_dir"
|
||
fi
|
||
print_success "All project files updated from GitHub"
|
||
return 0
|
||
}
|
||
|
||
# After update replaces betterdesk.sh on disk, re-exec so Repair / Protocol Toggle
|
||
# use the new functions (bash keeps the old script in memory otherwise) (#219).
|
||
reexec_installer_after_update() {
|
||
if [ "${AUTO_MODE:-false}" = "true" ]; then
|
||
return 0
|
||
fi
|
||
if [ "${BETTERDESK_REEXECED:-}" = "1" ]; then
|
||
return 0
|
||
fi
|
||
local self="${SCRIPT_DIR}/betterdesk.sh"
|
||
if [ ! -f "$self" ]; then
|
||
self="${BASH_SOURCE[0]}"
|
||
fi
|
||
print_info "Reloading installer so the next menu action uses the updated betterdesk.sh (#219)"
|
||
press_enter
|
||
exec env BETTERDESK_REEXECED=1 bash "$self" "${BETTERDESK_ORIG_ARGV[@]}"
|
||
}
|
||
|
||
# If Update already wrote a newer betterdesk.sh, re-exec before Repair/Toggle (#219).
|
||
maybe_reexec_if_installer_on_disk_is_newer() {
|
||
if [ "${AUTO_MODE:-false}" = "true" ]; then
|
||
return 0
|
||
fi
|
||
if [ "${BETTERDESK_REEXECED:-}" = "1" ]; then
|
||
return 0
|
||
fi
|
||
local self="${SCRIPT_DIR}/betterdesk.sh"
|
||
[ -f "$self" ] || return 0
|
||
local disk_rev
|
||
disk_rev=$(grep -m1 '^BETTERDESK_SH_REVISION=' "$self" 2>/dev/null | cut -d= -f2- | tr -d "\"'[:space:]")
|
||
if [ -z "$disk_rev" ] || [ "$disk_rev" = "${BETTERDESK_SH_REVISION:-}" ]; then
|
||
return 0
|
||
fi
|
||
print_info "Installer on disk is newer (revision $disk_rev) — reloading before this action (#219)"
|
||
exec env BETTERDESK_REEXECED=1 bash "$self" "${BETTERDESK_ORIG_ARGV[@]}"
|
||
}
|
||
|
||
do_update() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ UPDATE ══════════${NC}"
|
||
echo ""
|
||
|
||
detect_installation
|
||
|
||
if [ "$INSTALL_STATUS" = "none" ]; then
|
||
print_error "BetterDesk is not installed!"
|
||
print_info "Use 'FRESH INSTALLATION' option"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
# Detect Rust → Go upgrade (major architecture change)
|
||
if [ "${SERVER_TYPE:-}" = "rust" ]; then
|
||
print_warning "Legacy Rust server (hbbs/hbbr) detected!"
|
||
print_warning "Upgrading from Rust to Go server requires a FRESH INSTALLATION."
|
||
print_info "The Go server is a single binary replacing both hbbs and hbbr."
|
||
print_info "Your data (keys, database) will be preserved during migration."
|
||
echo ""
|
||
if [ "${AUTO_MODE:-false}" = "true" ]; then
|
||
print_info "Auto mode: Redirecting to fresh installation for Rust → Go migration"
|
||
do_install
|
||
return
|
||
else
|
||
read -rp "Proceed with fresh installation (recommended)? [Y/n] " answer
|
||
if [ "${answer,,}" != "n" ]; then
|
||
do_install
|
||
return
|
||
else
|
||
print_warning "Continuing with update — legacy Rust binaries will NOT be replaced with Go server."
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# CRITICAL: Preserve database configuration before reinstalling console
|
||
# This prevents PostgreSQL → SQLite switch during updates
|
||
preserve_database_config
|
||
|
||
# ---- Update method selection ----
|
||
read_update_github_branch_from_env
|
||
print_info "GitHub update branch: $UPDATE_GITHUB_BRANCH"
|
||
|
||
if [ "${AUTO_MODE:-false}" = "true" ]; then
|
||
print_info "Auto mode: using GitHub pull update"
|
||
else
|
||
local _menu_items=(
|
||
$'Online update from GitHub\tDownload latest code, rebuild server, update console'
|
||
$'In-app updater\tBuilt-in Node.js commit-aware updater'
|
||
$'Local update\tCopy files from this script\'s directory'
|
||
$'Switch update channel\tChoose stable (main) or development (dev) branch'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 0 )
|
||
menu_choose "Update Method" "Online GitHub update is recommended"
|
||
update_method="${MENU_CHOICE:-1}"
|
||
|
||
case "$update_method" in
|
||
0)
|
||
return
|
||
;;
|
||
4)
|
||
switch_update_channel
|
||
return
|
||
;;
|
||
2)
|
||
if run_terminal_project_update; then
|
||
print_success "Online project update completed"
|
||
reexec_installer_after_update
|
||
return
|
||
else
|
||
update_rc=$?
|
||
if [ "$update_rc" -ne 2 ]; then
|
||
print_error "In-app update failed (exit code: $update_rc)"
|
||
else
|
||
print_error "In-app updater not available (Node.js or CLI script missing)"
|
||
fi
|
||
press_enter
|
||
return
|
||
fi
|
||
;;
|
||
3)
|
||
# Legacy local update path
|
||
print_info "Using local files from: $SCRIPT_DIR"
|
||
print_info "Creating backup before update..."
|
||
do_backup_silent
|
||
graceful_stop_services
|
||
detect_architecture
|
||
install_binaries true
|
||
install_console
|
||
run_migrations
|
||
maybe_update_services
|
||
prepare_console_after_update
|
||
maybe_create_admin_user_on_update
|
||
if ! start_services_with_verification; then
|
||
print_error "Local update applied but services did not start correctly"
|
||
press_enter
|
||
return 1
|
||
fi
|
||
print_success "Local update completed!"
|
||
reexec_installer_after_update
|
||
return
|
||
;;
|
||
1|*)
|
||
# Fall through to GitHub update below
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# ---- GitHub Pull Update ----
|
||
print_info "Creating backup before update..."
|
||
do_backup_silent
|
||
|
||
# Stop services gracefully before updating files
|
||
graceful_stop_services
|
||
|
||
if ! update_from_github; then
|
||
print_error "GitHub update failed"
|
||
print_info "Attempting to restart services with existing files..."
|
||
if ! start_services_with_verification; then
|
||
print_error "Could not restart services after failed update"
|
||
fi
|
||
press_enter
|
||
return 1
|
||
fi
|
||
|
||
# Run database migrations (adds missing columns etc.)
|
||
run_migrations
|
||
|
||
local svc_mode="default"
|
||
if [ "${AUTO_MODE:-false}" != true ]; then
|
||
echo ""
|
||
read -rp "Recreate systemd service units from installer template? [y/N] " _recreate_svc
|
||
if [ "${_recreate_svc,,}" = "y" ] || [ "${_recreate_svc,,}" = "yes" ]; then
|
||
svc_mode="recreate"
|
||
fi
|
||
fi
|
||
|
||
# Patch existing units or create missing; optional full recreate (issue #158)
|
||
maybe_update_services "$svc_mode"
|
||
|
||
prepare_console_after_update || print_warning "Console prep after update reported issues"
|
||
maybe_create_admin_user_on_update
|
||
|
||
# Start services with verification (#306 — do not claim success if Console is down)
|
||
if ! start_services_with_verification; then
|
||
print_error "Update files applied but services did not start correctly"
|
||
print_info "Fix Console with: sudo systemctl start betterdesk-console"
|
||
print_info "Logs: journalctl -u betterdesk-console -n 50 --no-pager"
|
||
press_enter
|
||
return 1
|
||
fi
|
||
|
||
print_success "Update completed!"
|
||
if [ -n "${remote_version:-}" ]; then
|
||
print_info "BetterDesk is now at version $remote_version"
|
||
fi
|
||
reexec_installer_after_update
|
||
}
|
||
|
||
#===============================================================================
|
||
# Repair Functions
|
||
#===============================================================================
|
||
|
||
do_repair() {
|
||
maybe_reexec_if_installer_on_disk_is_newer
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ REPAIR INSTALLATION ══════════${NC}"
|
||
echo ""
|
||
|
||
detect_installation
|
||
|
||
# CRITICAL: Preserve database configuration before any repair operation
|
||
# This prevents PostgreSQL → SQLite switch when regenerating service files
|
||
preserve_database_config
|
||
|
||
print_status
|
||
|
||
local _menu_items=(
|
||
$'Repair binaries\tReplace the server binary with BetterDesk Go'
|
||
$'Repair database\tAdd missing columns / run migrations'
|
||
$'Repair services\tRegenerate systemd service units'
|
||
$'Repair permissions\tFix file ownership and permissions'
|
||
$'Repair HTTPS / TLS\tFix LE certs, signal port :5000 conflict (#219)'
|
||
$'Full repair\tRun all repair steps above'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 5 6 0 )
|
||
menu_choose "Repair Installation" "Choose what to repair"
|
||
local repair_choice="$MENU_CHOICE"
|
||
|
||
case $repair_choice in
|
||
1) repair_binaries ;;
|
||
2) repair_database ;;
|
||
3) repair_services ;;
|
||
4) repair_permissions ;;
|
||
5) repair_https_tls ;;
|
||
6)
|
||
repair_binaries
|
||
repair_database
|
||
repair_services
|
||
repair_permissions
|
||
repair_https_tls
|
||
print_success "Full repair completed!"
|
||
;;
|
||
0) return ;;
|
||
esac
|
||
|
||
press_enter
|
||
}
|
||
|
||
repair_binaries() {
|
||
print_step "Repairing BetterDesk Go Server..."
|
||
|
||
detect_architecture
|
||
|
||
local go_binary="$GO_SERVER_SOURCE/betterdesk-server"
|
||
|
||
# Check if Go binary exists, or compile it
|
||
if [ ! -f "$go_binary" ]; then
|
||
print_info "Go server binary not found, checking if we can compile..."
|
||
|
||
if ! check_go_installed; then
|
||
print_info "Installing Go toolchain..."
|
||
if ! install_golang; then
|
||
print_error "Failed to install Go toolchain"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
if ! compile_go_server; then
|
||
print_error "Failed to compile Go server"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Create backup before repair
|
||
if [ -f "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
cp "$RUSTDESK_PATH/betterdesk-server" "$RUSTDESK_PATH/betterdesk-server.backup.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
fi
|
||
|
||
# Gracefully stop all services
|
||
graceful_stop_services
|
||
|
||
# Extra safety: wait and verify files are not in use
|
||
sleep 2
|
||
|
||
# Check if binary is still locked (Text file busy prevention)
|
||
if lsof "$RUSTDESK_PATH/betterdesk-server" 2>/dev/null | grep -q .; then
|
||
print_error "betterdesk-server binary is still in use!"
|
||
kill_stale_processes "betterdesk-server"
|
||
sleep 2
|
||
fi
|
||
|
||
# Now install binary
|
||
if ! install_binaries; then
|
||
print_error "Failed to install binary"
|
||
return 1
|
||
fi
|
||
|
||
# Start services with health verification
|
||
if ! start_services_with_verification; then
|
||
print_error "Services failed to start after binary repair"
|
||
print_info "Check logs above for details"
|
||
return 1
|
||
fi
|
||
|
||
print_success "Go server binary repaired and services verified!"
|
||
}
|
||
|
||
repair_database() {
|
||
print_step "Repair database..."
|
||
|
||
if [ ! -f "$DB_PATH" ]; then
|
||
print_warning "Database does not exist, creating new one..."
|
||
touch "$DB_PATH"
|
||
fi
|
||
|
||
# Add missing columns
|
||
python3 << EOF
|
||
import sqlite3
|
||
|
||
conn = sqlite3.connect('$DB_PATH')
|
||
cursor = conn.cursor()
|
||
|
||
# Ensure peer table has required columns
|
||
columns_to_add = [
|
||
('status', 'INTEGER DEFAULT 0'),
|
||
('last_online', 'TEXT'),
|
||
('is_deleted', 'INTEGER DEFAULT 0'),
|
||
('deleted_at', 'TEXT'),
|
||
('updated_at', 'TEXT'),
|
||
('note', 'TEXT'),
|
||
('previous_ids', 'TEXT'),
|
||
('id_changed_at', 'TEXT'),
|
||
]
|
||
|
||
cursor.execute("PRAGMA table_info(peer)")
|
||
existing_columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
for col_name, col_def in columns_to_add:
|
||
if col_name not in existing_columns:
|
||
try:
|
||
cursor.execute(f"ALTER TABLE peer ADD COLUMN {col_name} {col_def}")
|
||
print(f" Added column: {col_name}")
|
||
except Exception as e:
|
||
pass
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
print("Database repaired")
|
||
EOF
|
||
|
||
print_success "Database repaired"
|
||
}
|
||
|
||
repair_services() {
|
||
print_step "Repairing systemd services..."
|
||
|
||
# Stop services gracefully first
|
||
graceful_stop_services
|
||
|
||
# Backup existing service files
|
||
for svc in betterdesk-server betterdesk-console rustdesksignal rustdeskrelay betterdesk; do
|
||
if [ -f "/etc/systemd/system/${svc}.service" ]; then
|
||
cp "/etc/systemd/system/${svc}.service" "/etc/systemd/system/${svc}.service.backup.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# Verify Go server binary exists
|
||
if [ ! -f "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
print_error "betterdesk-server binary not found at $RUSTDESK_PATH/betterdesk-server"
|
||
print_info "Run 'Repair binaries' first"
|
||
return 1
|
||
fi
|
||
|
||
# Regenerate service files
|
||
setup_services
|
||
|
||
# Start services with health verification
|
||
if ! start_services_with_verification; then
|
||
print_error "Services failed to start after repair"
|
||
print_info "Restoring backup service files..."
|
||
|
||
for svc in betterdesk-server betterdesk-console; do
|
||
backup_file=$(ls -t /etc/systemd/system/${svc}.service.backup.* 2>/dev/null | head -1)
|
||
if [ -n "$backup_file" ]; then
|
||
cp "$backup_file" "/etc/systemd/system/${svc}.service"
|
||
fi
|
||
done
|
||
systemctl daemon-reload
|
||
|
||
return 1
|
||
fi
|
||
|
||
print_success "Services repaired and verified!"
|
||
}
|
||
|
||
repair_permissions() {
|
||
print_step "Repairing permissions..."
|
||
|
||
if [ -f "$CONSOLE_PATH/server.js" ]; then
|
||
local console_user
|
||
console_user=$(ensure_betterdesk_console_user)
|
||
print_info "Console tree owner: $console_user"
|
||
if [ -f "$CONSOLE_PATH/scripts/linux-ensure-console-user.js" ] && command -v node &>/dev/null; then
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
node "$CONSOLE_PATH/scripts/linux-ensure-console-user.js" || print_warning "Console permission script reported issues"
|
||
else
|
||
print_warning "Console permission sync skipped; run linux-ensure-console-user.js as root"
|
||
fi
|
||
fi
|
||
repair_https_stuck_state yes
|
||
ensure_console_tls_material_readable 2>/dev/null || true
|
||
fi
|
||
|
||
chmod +x "$RUSTDESK_PATH/betterdesk-server" 2>/dev/null || true
|
||
|
||
if systemctl is-enabled --quiet betterdesk-console 2>/dev/null; then
|
||
systemctl restart betterdesk-console 2>/dev/null || true
|
||
sleep 2
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null; then
|
||
print_success "Permissions repaired — console is running"
|
||
else
|
||
print_warning "Permissions synced but console still inactive — check: journalctl -u betterdesk-console -n 40 --no-pager"
|
||
fi
|
||
else
|
||
print_success "Permissions repaired"
|
||
fi
|
||
}
|
||
|
||
repair_https_tls() {
|
||
print_step "Repairing HTTPS / TLS configuration (#219)..."
|
||
|
||
repair_https_stuck_state
|
||
|
||
if confirm "Restart BetterDesk services now?"; then
|
||
systemctl restart betterdesk-server betterdesk-console 2>/dev/null || true
|
||
verify_service_health "betterdesk-server" "21116" 15 >/dev/null 2>&1 || true
|
||
verify_service_health "betterdesk-console" "$(resolve_panel_health_port)" 15 >/dev/null 2>&1 || true
|
||
print_success "BetterDesk services restarted"
|
||
run_protocol_tests
|
||
else
|
||
print_info "Repair saved. Restart later: systemctl restart betterdesk-server betterdesk-console"
|
||
fi
|
||
}
|
||
|
||
#===============================================================================
|
||
# Validation Functions
|
||
#===============================================================================
|
||
|
||
do_validate() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ INSTALLATION VALIDATION ══════════${NC}"
|
||
echo ""
|
||
|
||
local errors=0
|
||
local warnings=0
|
||
|
||
detect_installation
|
||
detect_architecture
|
||
|
||
echo -e "${WHITE}Checking components...${NC}"
|
||
echo ""
|
||
|
||
# Check directories
|
||
echo -n " RustDesk directory ($RUSTDESK_PATH): "
|
||
if [ -d "$RUSTDESK_PATH" ]; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${RED}✗ Not found${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
echo -n " Console directory ($CONSOLE_PATH): "
|
||
if [ -d "$CONSOLE_PATH" ]; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${RED}✗ Not found${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
# Check Go server binary
|
||
echo -n " BetterDesk Server (Go): "
|
||
if [ -x "$RUSTDESK_PATH/betterdesk-server" ]; then
|
||
echo -e "${GREEN}✓ Single binary (signal + relay + API)${NC}"
|
||
elif [ -x "$RUSTDESK_PATH/hbbs" ] && [ -x "$RUSTDESK_PATH/hbbr" ]; then
|
||
echo -e "${YELLOW}! Legacy Rust binaries (consider upgrading to Go)${NC}"
|
||
warnings=$((warnings + 1))
|
||
else
|
||
echo -e "${RED}✗ Not found or missing permissions${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
# Check database
|
||
echo -n " Database: "
|
||
local validate_db_type="sqlite"
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
validate_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
validate_db_type="${validate_db_type:-sqlite}"
|
||
fi
|
||
|
||
if [ "$validate_db_type" = "postgres" ]; then
|
||
local pg_uri
|
||
pg_uri=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
if [ -n "$pg_uri" ] && PGCONNECT_TIMEOUT=3 psql "$pg_uri" -c "SELECT 1" &>/dev/null 2>&1; then
|
||
echo -e "${GREEN}✓ PostgreSQL${NC}"
|
||
# Check tables in PostgreSQL
|
||
echo -n " - Table peers: "
|
||
if PGCONNECT_TIMEOUT=3 psql "$pg_uri" -c "SELECT 1 FROM peers LIMIT 1" &>/dev/null 2>&1; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${YELLOW}! Empty or not found (will be created on first start)${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
echo -n " - Table users: "
|
||
if PGCONNECT_TIMEOUT=3 psql "$pg_uri" -c "SELECT 1 FROM users LIMIT 1" &>/dev/null 2>&1; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${YELLOW}! Empty or not found (will be created on first start)${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ PostgreSQL connection failed${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
elif [ -f "$DB_PATH" ]; then
|
||
echo -e "${GREEN}✓ SQLite${NC}"
|
||
|
||
# Check tables (Go uses 'peers', legacy uses 'peer')
|
||
echo -n " - Table peers: "
|
||
if sqlite3 "$DB_PATH" "SELECT 1 FROM peers LIMIT 1" 2>/dev/null; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
elif sqlite3 "$DB_PATH" "SELECT 1 FROM peer LIMIT 1" 2>/dev/null; then
|
||
echo -e "${YELLOW}! Legacy schema (peer)${NC}"
|
||
warnings=$((warnings + 1))
|
||
else
|
||
echo -e "${YELLOW}! Empty or not found (will be created on first start)${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
|
||
echo -n " - Table users: "
|
||
if sqlite3 "$DB_PATH" "SELECT 1 FROM users LIMIT 1" 2>/dev/null; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${YELLOW}! Empty or not found (will be created on first start)${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
else
|
||
# Check if Go server is running — it creates the DB on start
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
echo -e "${YELLOW}! SQLite file not yet created (server running, will create on first connection)${NC}"
|
||
warnings=$((warnings + 1))
|
||
else
|
||
echo -e "${RED}✗ Not found (will be created when server starts)${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
fi
|
||
|
||
# Check keys
|
||
echo -n " Ed25519 key: "
|
||
if [ -f "$RUSTDESK_PATH/id_ed25519.pub" ] || [ -f "$RUSTDESK_PATH/id_ed25519" ]; then
|
||
echo -e "${GREEN}✓${NC}"
|
||
else
|
||
echo -e "${YELLOW}! Will be generated on first start${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
|
||
# Check services
|
||
echo ""
|
||
echo -e "${WHITE}Checking services...${NC}"
|
||
echo ""
|
||
|
||
# Check Go server service first
|
||
echo -n " betterdesk-server (Go): "
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
echo -e "${GREEN}● Active (signal + relay + API)${NC}"
|
||
elif systemctl is-enabled --quiet betterdesk-server 2>/dev/null; then
|
||
echo -e "${YELLOW}○ Enabled but inactive${NC}"
|
||
warnings=$((warnings + 1))
|
||
elif systemctl list-unit-files betterdesk-server.service &>/dev/null 2>&1; then
|
||
echo -e "${RED}○ Disabled${NC}"
|
||
errors=$((errors + 1))
|
||
else
|
||
# Check legacy Rust services
|
||
echo -e "${CYAN}Not installed${NC}"
|
||
|
||
for service in rustdesksignal rustdeskrelay; do
|
||
echo -n " $service (Legacy Rust): "
|
||
if systemctl is-active --quiet "$service" 2>/dev/null; then
|
||
echo -e "${GREEN}● Active${NC}"
|
||
elif systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||
echo -e "${YELLOW}○ Enabled but inactive${NC}"
|
||
warnings=$((warnings + 1))
|
||
else
|
||
echo -e "${RED}○ Disabled${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
done
|
||
fi
|
||
|
||
echo -n " betterdesk-console (Node.js): "
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null; then
|
||
echo -e "${GREEN}● Active${NC}"
|
||
elif systemctl is-active --quiet betterdesk 2>/dev/null; then
|
||
echo -e "${GREEN}● Active (legacy name)${NC}"
|
||
elif systemctl is-enabled --quiet betterdesk-console 2>/dev/null; then
|
||
echo -e "${YELLOW}○ Enabled but inactive${NC}"
|
||
warnings=$((warnings + 1))
|
||
else
|
||
echo -e "${RED}○ Disabled${NC}"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
# Check ports
|
||
echo ""
|
||
echo -e "${WHITE}Checking ports...${NC}"
|
||
echo ""
|
||
|
||
for port in "${GO_API_PORT:-21114}" "${CLIENT_API_PORT:-21121}" 21115 21116 21117 5000; do
|
||
echo -n " Port $port: "
|
||
if ss -tlnp 2>/dev/null | grep -q ":$port " || netstat -tlnp 2>/dev/null | grep -q ":$port "; then
|
||
local pname=$(ss -tlnp 2>/dev/null | grep ":$port " | grep -oP 'users:\(\("\K[^"]+' 2>/dev/null | head -1)
|
||
echo -e "${GREEN}● Listening${NC}${pname:+ ($pname)}"
|
||
else
|
||
echo -e "${YELLOW}○ Free${NC}"
|
||
warnings=$((warnings + 1))
|
||
fi
|
||
done
|
||
|
||
# Summary
|
||
echo ""
|
||
echo -e "${WHITE}═══════════════════════════════════════${NC}"
|
||
|
||
if [ $errors -eq 0 ] && [ $warnings -eq 0 ]; then
|
||
echo -e "${GREEN}✓ Installation correct - no problems found${NC}"
|
||
elif [ $errors -eq 0 ]; then
|
||
echo -e "${YELLOW}! Found $warnings warnings${NC}"
|
||
else
|
||
echo -e "${RED}✗ Found $errors errors and $warnings warnings${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}Use 'REPAIR INSTALLATION' option to fix problems${NC}"
|
||
fi
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Backup Functions
|
||
#===============================================================================
|
||
|
||
do_backup() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ BACKUP ══════════${NC}"
|
||
echo ""
|
||
|
||
do_backup_silent
|
||
|
||
print_success "Backup completed!"
|
||
press_enter
|
||
}
|
||
|
||
do_backup_silent() {
|
||
local backup_name="betterdesk_backup_$(date +%Y%m%d_%H%M%S)"
|
||
local backup_path="$BACKUP_DIR/$backup_name"
|
||
|
||
mkdir -p "$backup_path"
|
||
|
||
print_step "Creating backup: $backup_name"
|
||
|
||
# Backup database
|
||
if [ -f "$DB_PATH" ]; then
|
||
cp "$DB_PATH" "$backup_path/"
|
||
print_info " - Database"
|
||
fi
|
||
|
||
# Backup keys
|
||
if [ -f "$RUSTDESK_PATH/id_ed25519" ]; then
|
||
cp "$RUSTDESK_PATH/id_ed25519"* "$backup_path/"
|
||
print_info " - Keys"
|
||
fi
|
||
|
||
# Backup API key
|
||
if [ -f "$RUSTDESK_PATH/.api_key" ]; then
|
||
cp "$RUSTDESK_PATH/.api_key" "$backup_path/"
|
||
print_info " - API key"
|
||
fi
|
||
|
||
# Backup credentials
|
||
if [ -f "$RUSTDESK_PATH/.admin_credentials" ]; then
|
||
cp "$RUSTDESK_PATH/.admin_credentials" "$backup_path/"
|
||
print_info " - Login credentials"
|
||
fi
|
||
|
||
# Create archive
|
||
cd "$BACKUP_DIR"
|
||
tar -czf "$backup_name.tar.gz" "$backup_name"
|
||
rm -rf "$backup_name"
|
||
|
||
print_success "Backup saved: $BACKUP_DIR/$backup_name.tar.gz"
|
||
}
|
||
|
||
#===============================================================================
|
||
# Password Reset Functions
|
||
#===============================================================================
|
||
|
||
do_reset_password() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ ADMIN PASSWORD RESET ══════════${NC}"
|
||
echo ""
|
||
|
||
# Refresh detection
|
||
auto_detect_paths
|
||
|
||
if [ "$CONSOLE_TYPE" = "none" ]; then
|
||
print_error "No console installation detected!"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
echo -e "Detected console type: ${CYAN}${CONSOLE_TYPE}${NC}"
|
||
echo ""
|
||
|
||
local _menu_items=(
|
||
$'Generate random password\tCreate a strong random admin password'
|
||
$'Set custom password\tType a new password (min. 8 characters)'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 0 )
|
||
menu_choose "Admin Password Reset" "Console type: ${CONSOLE_TYPE}"
|
||
local pw_choice="$MENU_CHOICE"
|
||
|
||
local new_password
|
||
|
||
case $pw_choice in
|
||
1)
|
||
# M-05: full hex entropy
|
||
new_password=$(openssl rand -hex 16)
|
||
;;
|
||
2)
|
||
echo ""
|
||
read -sp "Enter new password (min. 8 characters): " new_password
|
||
echo ""
|
||
if [ ${#new_password} -lt 8 ]; then
|
||
print_error "Password must be at least 8 characters!"
|
||
press_enter
|
||
return
|
||
fi
|
||
;;
|
||
0)
|
||
return
|
||
;;
|
||
*)
|
||
return
|
||
;;
|
||
esac
|
||
|
||
local success=false
|
||
|
||
if [ "$CONSOLE_TYPE" = "nodejs" ]; then
|
||
# --- Hotfix: detect broken Go-first auth flow (commit 188991d) ---
|
||
# If authService.js contains the broken Go-first authenticate() function,
|
||
# auto-download the fixed version. Without this, NO password will work.
|
||
local auth_service="$CONSOLE_PATH/services/authService.js"
|
||
if [ -f "$auth_service" ] && grep -q 'checkGoServerHealth.*authenticateViaGo' "$auth_service" 2>/dev/null; then
|
||
# Check if authenticate() delegates to Go first (broken pattern)
|
||
if grep -q 'const health = await checkGoServerHealth' "$auth_service" 2>/dev/null; then
|
||
print_warning "Detected broken authentication flow (Go-first delegation bug)"
|
||
print_info "Downloading fixed authService.js from GitHub..."
|
||
local fixed_url="https://raw.githubusercontent.com/UNITRONIX/BetterDesk/main/web-nodejs/services/authService.js"
|
||
if curl -fsSL "$fixed_url" -o "$auth_service.tmp" 2>/dev/null; then
|
||
# Verify the fix was downloaded correctly (check for local-first pattern)
|
||
if grep -q 'Step 1: Check local database FIRST' "$auth_service.tmp" 2>/dev/null; then
|
||
mv "$auth_service.tmp" "$auth_service"
|
||
print_success "Fixed authentication flow (restored local-first login)"
|
||
else
|
||
rm -f "$auth_service.tmp"
|
||
print_warning "Downloaded file does not contain expected fix — skipped"
|
||
fi
|
||
else
|
||
rm -f "$auth_service.tmp" 2>/dev/null
|
||
print_warning "Could not download fix (no internet?) — password reset will proceed but login may still fail"
|
||
print_info "Manual fix: curl -sL '$fixed_url' -o '$auth_service'"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Detect database type from console .env
|
||
local db_type="sqlite"
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
local env_db_type
|
||
env_db_type=$(grep -E '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '[:space:]')
|
||
if [ "$env_db_type" = "postgres" ] || [ "$env_db_type" = "postgresql" ]; then
|
||
db_type="postgres"
|
||
fi
|
||
fi
|
||
|
||
print_info "Database type: $db_type"
|
||
|
||
# Use Node.js reset-password script (supports both SQLite and PostgreSQL)
|
||
local reset_script="$CONSOLE_PATH/scripts/reset-password.js"
|
||
if [ -f "$reset_script" ] && command -v node &> /dev/null; then
|
||
print_info "Using reset-password.js script..."
|
||
pushd "$CONSOLE_PATH" > /dev/null
|
||
# The script reads .env for DB_TYPE and DATABASE_URL automatically
|
||
DATA_DIR="$CONSOLE_PATH/data" node "$reset_script" "$new_password" admin
|
||
if [ $? -eq 0 ]; then
|
||
success=true
|
||
fi
|
||
popd > /dev/null
|
||
fi
|
||
|
||
# Fallback: direct database update
|
||
if [ "$success" = "false" ]; then
|
||
if [ "$db_type" = "postgres" ]; then
|
||
# PostgreSQL mode — use psql or Python with psycopg2
|
||
local pg_url
|
||
pg_url=$(grep -E '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
||
|
||
if [ -n "$pg_url" ] && command -v python3 &> /dev/null; then
|
||
print_info "Using Python to update PostgreSQL..."
|
||
PG_URL="$pg_url" RESET_ADMIN_PASSWORD="$new_password" python3 << 'PYEOF'
|
||
import bcrypt
|
||
import os
|
||
try:
|
||
import psycopg2
|
||
except ImportError:
|
||
import subprocess, sys
|
||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'psycopg2-binary', '-q'])
|
||
import psycopg2
|
||
|
||
pg_url = os.environ.get('PG_URL', '')
|
||
new_password = os.environ.get('RESET_ADMIN_PASSWORD', '')
|
||
password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt(12)).decode()
|
||
|
||
conn = psycopg2.connect(pg_url)
|
||
cursor = conn.cursor()
|
||
|
||
# Create table if missing
|
||
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
|
||
id SERIAL PRIMARY KEY,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
role TEXT DEFAULT 'admin',
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
last_login TIMESTAMPTZ
|
||
)''')
|
||
|
||
cursor.execute("UPDATE users SET password_hash = %s WHERE username = 'admin'", (password_hash,))
|
||
|
||
if cursor.rowcount == 0:
|
||
cursor.execute("INSERT INTO users (username, password_hash, role) VALUES ('admin', %s, 'admin')", (password_hash,))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
print("Password updated successfully (PostgreSQL)")
|
||
PYEOF
|
||
if [ $? -eq 0 ]; then
|
||
success=true
|
||
fi
|
||
fi
|
||
else
|
||
# SQLite mode — update auth.db directly
|
||
local auth_db_path="$CONSOLE_PATH/data/auth.db"
|
||
if [ ! -f "$auth_db_path" ]; then
|
||
auth_db_path="$RUSTDESK_PATH/auth.db"
|
||
fi
|
||
print_info "Auth database: $auth_db_path"
|
||
|
||
AUTH_DB_PATH="$auth_db_path" RESET_ADMIN_PASSWORD="$new_password" python3 << 'PYEOF'
|
||
import sqlite3
|
||
import bcrypt
|
||
import os
|
||
|
||
auth_db_path = os.environ.get('AUTH_DB_PATH', '')
|
||
|
||
# Create parent directory if needed
|
||
os.makedirs(os.path.dirname(auth_db_path), exist_ok=True)
|
||
|
||
conn = sqlite3.connect(auth_db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# Ensure table exists (for fresh installations)
|
||
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
role TEXT DEFAULT 'admin',
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
last_login TEXT
|
||
)''')
|
||
|
||
new_password = os.environ.get('RESET_ADMIN_PASSWORD', '')
|
||
password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt(12)).decode()
|
||
|
||
cursor.execute("UPDATE users SET password_hash = ? WHERE username = 'admin'", (password_hash,))
|
||
|
||
if cursor.rowcount == 0:
|
||
cursor.execute('''INSERT INTO users (username, password_hash, role)
|
||
VALUES ('admin', ?, 'admin')''', (password_hash,))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
print("Password updated successfully")
|
||
PYEOF
|
||
if [ $? -eq 0 ]; then
|
||
success=true
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
if [ "$success" = "true" ]; then
|
||
# Update DEFAULT_ADMIN_PASSWORD in .env so ensureDefaultAdmin() does not
|
||
# overwrite the new hash on next Node.js restart
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
if grep -q '^DEFAULT_ADMIN_PASSWORD=' "$CONSOLE_PATH/.env" 2>/dev/null; then
|
||
# Use awk to safely handle passwords with special chars (|, &, $, etc.)
|
||
awk -v pw="$new_password" '/^DEFAULT_ADMIN_PASSWORD=/{print "DEFAULT_ADMIN_PASSWORD=" pw; next}{print}' "$CONSOLE_PATH/.env" > "$CONSOLE_PATH/.env.tmp" && mv "$CONSOLE_PATH/.env.tmp" "$CONSOLE_PATH/.env"
|
||
fi
|
||
fi
|
||
|
||
# Restart console so it picks up the new .env value
|
||
if systemctl is-active betterdesk-console &>/dev/null; then
|
||
print_info "Restarting betterdesk-console..."
|
||
systemctl restart betterdesk-console 2>/dev/null || true
|
||
sleep 2
|
||
fi
|
||
|
||
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ NEW LOGIN CREDENTIALS ║${NC}"
|
||
echo -e "${GREEN}╠════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ Login: ${WHITE}admin${GREEN} ║${NC}"
|
||
echo -e "${GREEN}║ Password: ${WHITE}${new_password}${GREEN} ║${NC}"
|
||
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
|
||
|
||
# Persist credentials only when explicitly requested.
|
||
if [ "$STORE_ADMIN_CREDENTIALS" = "true" ]; then
|
||
cat > "$RUSTDESK_PATH/.admin_credentials" << CREDEOF
|
||
Admin Username: admin
|
||
Admin Password: $new_password
|
||
Generated by: BetterDesk password reset
|
||
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
CREDEOF
|
||
chmod 600 "$RUSTDESK_PATH/.admin_credentials"
|
||
fi
|
||
else
|
||
print_error "Failed to reset password!"
|
||
print_info "Make sure Python with bcrypt is installed, or Node.js for Node.js console"
|
||
fi
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Build Functions
|
||
#===============================================================================
|
||
|
||
# Legacy Support Agent source staging — Generator now downloads BetterDesk-Client
|
||
# templates into data/modules/betterdesk-support-generator/. Kept as a no-op so
|
||
# older install/update call sites do not fail.
|
||
stage_support_agent_source() {
|
||
print_info "Support Generator uses Client templates (module install); skipping Go support-agent staging"
|
||
return 0
|
||
}
|
||
|
||
# Install the toolchain used by the Node.js console worker to compile branded
|
||
# agent installers (cargo + rustup + tauri-cli + cargo-xwin + nsis + rpm +
|
||
# appimagetool + mingw-w64). Runs the standalone script shipped alongside the
|
||
# installer; idempotent. Requires ~3 GB download + 5 GB free disk.
|
||
do_install_build_toolchain() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ AGENT BUILD TOOLCHAIN ══════════${NC}"
|
||
echo ""
|
||
echo " Installs: Rust stable, cargo-tauri 2.x, cargo-xwin,"
|
||
echo " mingw-w64 (Windows cross-compile), NSIS,"
|
||
echo " rpm-build, appimagetool, pnpm, WebKit dev libs."
|
||
echo ""
|
||
echo " Disk: ~3 GB download, ~5 GB after install,"
|
||
echo " plus cargo build cache (/var/cache/betterdesk-build)."
|
||
echo ""
|
||
echo " This is REQUIRED for the 'Generator Agenta' feature."
|
||
echo " Skip if you do not generate branded agent installers."
|
||
echo ""
|
||
|
||
local script_dir
|
||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
local toolchain_script="$script_dir/scripts/install-build-toolchain.sh"
|
||
|
||
if [ ! -f "$toolchain_script" ]; then
|
||
print_error "Toolchain installer not found: $toolchain_script"
|
||
print_info "Pull the latest repository and try again."
|
||
press_enter
|
||
return 1
|
||
fi
|
||
|
||
if [ "$AUTO_MODE" != true ]; then
|
||
read -p "Proceed with toolchain install? [y/N]: " confirm
|
||
confirm="${confirm:-N}"
|
||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||
print_info "Cancelled."
|
||
press_enter
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
local extra_args=()
|
||
[ "$AUTO_MODE" = true ] && extra_args+=(--unattended)
|
||
|
||
BUILD_USER="${SUDO_USER:-${BUILD_USER:-unitronix}}" \
|
||
bash "$toolchain_script" "${extra_args[@]}"
|
||
|
||
local rc=$?
|
||
if [ $rc -eq 0 ]; then
|
||
stage_support_agent_source "$script_dir" || true
|
||
print_success "Build toolchain installed."
|
||
print_info "Restart the console service to pick up new PATH:"
|
||
print_info " sudo systemctl restart betterdesk-console"
|
||
else
|
||
print_error "Toolchain installer exited with code $rc"
|
||
fi
|
||
press_enter
|
||
return $rc
|
||
}
|
||
|
||
do_build() {
|
||
local _menu_items=(
|
||
$'Rebuild & deploy server\tCompile, stop, replace and restart the Go server'
|
||
$'Compile server only\tBuild the Go binary without deploying it'
|
||
$'Build legacy Rust binaries\tArchived hbbs/hbbr (advanced)'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 0 )
|
||
menu_choose "Build & Deploy" "Rebuild and deploy the BetterDesk Go server"
|
||
local build_choice="${MENU_CHOICE:-1}"
|
||
|
||
case $build_choice in
|
||
1) do_rebuild_go_server ;;
|
||
2) do_compile_go_only ;;
|
||
3) do_build_legacy_rust ;;
|
||
0) return ;;
|
||
*) print_warning "Invalid option"; sleep 1 ;;
|
||
esac
|
||
}
|
||
|
||
# Rebuild & deploy Go server: compile → backup → stop → replace → start → verify
|
||
do_rebuild_go_server() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ REBUILD & DEPLOY GO SERVER ══════════${NC}"
|
||
echo ""
|
||
|
||
detect_installation
|
||
|
||
if [ "$INSTALL_STATUS" = "none" ]; then
|
||
print_warning "BetterDesk is not installed. Binary will be compiled but not deployed."
|
||
if ! confirm "Continue with compilation only?"; then
|
||
press_enter
|
||
return
|
||
fi
|
||
do_compile_go_only
|
||
return
|
||
fi
|
||
|
||
# Step 1: Compile
|
||
print_step "[1/5] Compiling Go server from source..."
|
||
detect_architecture
|
||
|
||
if ! compile_go_server; then
|
||
print_error "Compilation failed — aborting. Current installation is untouched."
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
local new_binary="$GO_SERVER_SOURCE/betterdesk-server"
|
||
if [ ! -f "$new_binary" ]; then
|
||
print_error "Compiled binary not found at $new_binary"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
# Step 2: Backup current binary
|
||
print_step "[2/5] Backing up current binary..."
|
||
local installed_binary="$RUSTDESK_PATH/betterdesk-server"
|
||
local ts
|
||
ts=$(date +%Y%m%d_%H%M%S)
|
||
if [ -f "$installed_binary" ]; then
|
||
cp "$installed_binary" "${installed_binary}.backup.${ts}"
|
||
print_info "Backup: ${installed_binary}.backup.${ts}"
|
||
else
|
||
print_info "No existing binary to backup"
|
||
fi
|
||
|
||
# Step 3: Stop services
|
||
print_step "[3/5] Stopping services..."
|
||
graceful_stop_services
|
||
|
||
# Step 4: Replace binary
|
||
print_step "[4/5] Deploying new binary..."
|
||
mkdir -p "$RUSTDESK_PATH"
|
||
cp "$new_binary" "$installed_binary"
|
||
chmod +x "$installed_binary"
|
||
local size
|
||
size=$(du -h "$installed_binary" | cut -f1)
|
||
print_success "Deployed: $installed_binary ($size)"
|
||
|
||
# Step 5: Start services and verify
|
||
print_step "[5/5] Starting services..."
|
||
start_services_with_verification
|
||
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
echo ""
|
||
print_success "Go server rebuilt and deployed successfully!"
|
||
echo ""
|
||
echo -e "${WHITE}Recent logs:${NC}"
|
||
journalctl -u betterdesk-server -n 5 --no-pager 2>/dev/null || true
|
||
else
|
||
print_error "Service failed to start after rebuild!"
|
||
echo ""
|
||
echo -e "${YELLOW}Rolling back to previous binary...${NC}"
|
||
if [ -f "${installed_binary}.backup.${ts}" ]; then
|
||
cp "${installed_binary}.backup.${ts}" "$installed_binary"
|
||
chmod +x "$installed_binary"
|
||
systemctl start betterdesk-server 2>/dev/null || true
|
||
sleep 2
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
print_success "Rollback successful — previous binary restored"
|
||
else
|
||
print_error "Rollback also failed. Check: journalctl -u betterdesk-server -n 50"
|
||
fi
|
||
else
|
||
print_error "No backup to rollback to. Check: journalctl -u betterdesk-server -n 50"
|
||
fi
|
||
fi
|
||
|
||
press_enter
|
||
}
|
||
|
||
# Compile Go server only (no deployment)
|
||
do_compile_go_only() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ COMPILE GO SERVER ══════════${NC}"
|
||
echo ""
|
||
|
||
detect_architecture
|
||
|
||
if ! compile_go_server; then
|
||
print_error "Compilation failed"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
local new_binary="$GO_SERVER_SOURCE/betterdesk-server"
|
||
local size
|
||
size=$(du -h "$new_binary" | cut -f1)
|
||
print_success "Binary compiled: $new_binary ($size)"
|
||
print_info "Use option 7 → 1 to deploy it, or copy manually."
|
||
|
||
press_enter
|
||
}
|
||
|
||
# Legacy Rust build (archived — hbbs/hbbr)
|
||
do_build_legacy_rust() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ BUILD LEGACY RUST BINARIES ══════════${NC}"
|
||
echo ""
|
||
print_warning "Legacy Rust binaries (hbbs/hbbr) are archived."
|
||
print_info "The Go server is the current architecture."
|
||
echo ""
|
||
if ! confirm "Continue with legacy Rust build anyway?"; then
|
||
return
|
||
fi
|
||
|
||
# Check Rust
|
||
if ! command -v cargo &> /dev/null; then
|
||
print_warning "Rust is not installed!"
|
||
echo ""
|
||
if confirm "Do you want to install Rust?"; then
|
||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||
source "$HOME/.cargo/env"
|
||
else
|
||
press_enter
|
||
return
|
||
fi
|
||
fi
|
||
|
||
print_info "Rust: $(cargo --version)"
|
||
echo ""
|
||
|
||
local build_dir="/tmp/betterdesk_build_$$"
|
||
mkdir -p "$build_dir"
|
||
cd "$build_dir"
|
||
|
||
print_step "Downloading RustDesk Server sources..."
|
||
git clone --depth 1 --branch 1.1.14 https://github.com/rustdesk/rustdesk-server.git
|
||
cd rustdesk-server
|
||
git submodule update --init --recursive
|
||
|
||
print_step "Applying BetterDesk modifications..."
|
||
|
||
# Copy modified sources
|
||
if [ -d "$SCRIPT_DIR/hbbs-patch-v2/src" ]; then
|
||
cp "$SCRIPT_DIR/hbbs-patch-v2/src/main.rs" src/ 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/hbbs-patch-v2/src/http_api.rs" src/ 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/hbbs-patch-v2/src/database.rs" src/ 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/hbbs-patch-v2/src/peer.rs" src/ 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/hbbs-patch-v2/src/rendezvous_server.rs" src/ 2>/dev/null || true
|
||
else
|
||
print_error "Modified sources not found in hbbs-patch-v2/src/"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
print_step "Compiling (may take several minutes)..."
|
||
cargo build --release
|
||
|
||
# Copy results
|
||
print_step "Copying binaries..."
|
||
detect_architecture
|
||
mkdir -p "$SCRIPT_DIR/hbbs-patch-v2"
|
||
|
||
cp target/release/hbbs "$SCRIPT_DIR/hbbs-patch-v2/hbbs-linux-$ARCH_NAME"
|
||
cp target/release/hbbr "$SCRIPT_DIR/hbbs-patch-v2/hbbr-linux-$ARCH_NAME"
|
||
|
||
# Cleanup
|
||
cd /
|
||
rm -rf "$build_dir"
|
||
|
||
print_success "Legacy Rust compilation completed!"
|
||
print_info "Binaries saved in: $SCRIPT_DIR/hbbs-patch-v2/"
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Firewall Configuration
|
||
#===============================================================================
|
||
|
||
configure_firewall_rules() {
|
||
local required_ports="${GO_API_PORT:-21114} ${CLIENT_API_PORT:-21121} 21115 21116 21117 21118 21119 5000 5443"
|
||
local created=0
|
||
local total=0
|
||
|
||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then
|
||
print_info "Configuring UFW firewall rules..."
|
||
|
||
for port in $required_ports; do
|
||
total=$((total + 1))
|
||
if ! ufw status 2>/dev/null | grep -qE "^${port}[/ ]"; then
|
||
if [ "$port" = "21116" ]; then
|
||
ufw allow 21116/tcp comment "BetterDesk ID Server TCP" 2>/dev/null && created=$((created + 1))
|
||
ufw allow 21116/udp comment "BetterDesk ID Server UDP" 2>/dev/null && created=$((created + 1))
|
||
total=$((total + 1))
|
||
else
|
||
ufw allow "${port}/tcp" comment "BetterDesk port ${port}" 2>/dev/null && created=$((created + 1))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
ufw reload 2>/dev/null
|
||
|
||
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld 2>/dev/null; then
|
||
print_info "Configuring firewalld rules..."
|
||
|
||
for port in $required_ports; do
|
||
total=$((total + 1))
|
||
local open_ports=$(firewall-cmd --list-ports 2>/dev/null)
|
||
if ! echo "$open_ports" | grep -qE "${port}/tcp"; then
|
||
if [ "$port" = "21116" ]; then
|
||
firewall-cmd --permanent --add-port=21116/tcp 2>/dev/null && created=$((created + 1))
|
||
firewall-cmd --permanent --add-port=21116/udp 2>/dev/null && created=$((created + 1))
|
||
total=$((total + 1))
|
||
else
|
||
firewall-cmd --permanent --add-port="${port}/tcp" 2>/dev/null && created=$((created + 1))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
firewall-cmd --reload 2>/dev/null
|
||
|
||
elif command -v iptables &>/dev/null; then
|
||
print_info "Configuring iptables rules..."
|
||
|
||
for port in $required_ports; do
|
||
total=$((total + 1))
|
||
if ! iptables -L INPUT -n 2>/dev/null | grep -qE "dpt:${port}\b"; then
|
||
if [ "$port" = "21116" ]; then
|
||
iptables -A INPUT -p tcp --dport 21116 -j ACCEPT 2>/dev/null && created=$((created + 1))
|
||
iptables -A INPUT -p udp --dport 21116 -j ACCEPT 2>/dev/null && created=$((created + 1))
|
||
total=$((total + 1))
|
||
else
|
||
iptables -A INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null && created=$((created + 1))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Try to persist iptables rules
|
||
if command -v iptables-save &>/dev/null; then
|
||
iptables-save > /etc/iptables/rules.v4 2>/dev/null || \
|
||
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
||
fi
|
||
else
|
||
print_info "No active firewall detected — no rules to configure"
|
||
return 0
|
||
fi
|
||
|
||
if [ $created -gt 0 ]; then
|
||
print_success "Created $created firewall rule(s)"
|
||
else
|
||
print_success "All firewall rules already configured"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
#===============================================================================
|
||
# Diagnostics Functions
|
||
#===============================================================================
|
||
|
||
do_diagnostics() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ DIAGNOSTICS ══════════${NC}"
|
||
echo ""
|
||
|
||
print_status
|
||
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Service logs (last 10 lines) ═══${NC}"
|
||
echo ""
|
||
|
||
# Check for Go server first, then legacy Rust services
|
||
if systemctl list-unit-files betterdesk-server.service &>/dev/null 2>&1; then
|
||
echo -e "${CYAN}--- betterdesk-server (Go) ---${NC}"
|
||
journalctl -u betterdesk-server -n 10 --no-pager 2>/dev/null || echo "No logs found"
|
||
else
|
||
echo -e "${CYAN}--- rustdesksignal (Legacy Rust) ---${NC}"
|
||
journalctl -u rustdesksignal -n 10 --no-pager 2>/dev/null || echo "No logs found"
|
||
|
||
echo ""
|
||
echo -e "${CYAN}--- rustdeskrelay (Legacy Rust) ---${NC}"
|
||
journalctl -u rustdeskrelay -n 10 --no-pager 2>/dev/null || echo "No logs found"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${CYAN}--- betterdesk-console (Node.js) ---${NC}"
|
||
journalctl -u betterdesk-console -n 10 --no-pager 2>/dev/null || \
|
||
journalctl -u betterdesk -n 10 --no-pager 2>/dev/null || echo "No logs found"
|
||
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Database statistics ═══${NC}"
|
||
echo ""
|
||
|
||
# Determine the active database type the SAME way the rest of the script does:
|
||
# read DB_TYPE from the console .env first (source of truth), and only fall back
|
||
# to SQLite file detection. This prevents a stale db_v2.sqlite3 left over from a
|
||
# previous install from masking an active PostgreSQL backend.
|
||
local diag_db_type="sqlite"
|
||
local diag_pg_uri=""
|
||
if [ -f "$CONSOLE_PATH/.env" ]; then
|
||
diag_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]')
|
||
diag_db_type="${diag_db_type:-sqlite}"
|
||
diag_pg_uri=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
fi
|
||
# The Go server may also carry the DSN in its systemd unit (-db postgres://...)
|
||
if [ "$diag_db_type" != "postgres" ] && [ -f /etc/systemd/system/betterdesk-server.service ]; then
|
||
local svc_db
|
||
svc_db=$(grep -oP '\-db\s+"?\K(postgres|postgresql)://[^" ]+' /etc/systemd/system/betterdesk-server.service 2>/dev/null | head -1)
|
||
if [ -n "$svc_db" ]; then
|
||
diag_db_type="postgres"
|
||
diag_pg_uri="${diag_pg_uri:-$svc_db}"
|
||
fi
|
||
fi
|
||
|
||
if [ "$diag_db_type" = "postgres" ] && [ -n "$diag_pg_uri" ]; then
|
||
# Mask password for display
|
||
local diag_pg_display
|
||
diag_pg_display=$(echo "$diag_pg_uri" | sed 's|://[^:]*:[^@]*@|://***:***@|')
|
||
echo -e " Database type: ${CYAN}PostgreSQL${NC}"
|
||
echo -e " Connection: ${DIM}$diag_pg_display${NC}"
|
||
if command -v psql &>/dev/null; then
|
||
if PGCONNECT_TIMEOUT=3 psql "$diag_pg_uri" -tAc "SELECT 1" &>/dev/null; then
|
||
local device_count online_count user_count
|
||
device_count=$(PGCONNECT_TIMEOUT=3 psql "$diag_pg_uri" -tAc "SELECT COUNT(*) FROM peers WHERE soft_deleted = FALSE" 2>/dev/null || echo "0")
|
||
online_count=$(PGCONNECT_TIMEOUT=3 psql "$diag_pg_uri" -tAc "SELECT COUNT(*) FROM peers WHERE soft_deleted = FALSE AND status = 'ONLINE'" 2>/dev/null || echo "0")
|
||
user_count=$(PGCONNECT_TIMEOUT=3 psql "$diag_pg_uri" -tAc "SELECT COUNT(*) FROM users" 2>/dev/null || echo "0")
|
||
echo -e " Status: ${GREEN}Connected${NC}"
|
||
echo " Devices: ${device_count:-0}"
|
||
echo " Online: ${online_count:-0}"
|
||
echo " Users: ${user_count:-0}"
|
||
else
|
||
echo -e " Status: ${RED}Connection failed${NC}"
|
||
echo -e " ${YELLOW}Tip: verify the PostgreSQL service and DATABASE_URL credentials${NC}"
|
||
fi
|
||
else
|
||
echo -e " ${YELLOW}Install the 'psql' client to see live database statistics${NC}"
|
||
fi
|
||
elif [ -f "$DB_PATH" ]; then
|
||
local device_count online_count user_count
|
||
device_count=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peers WHERE soft_deleted = 0" 2>/dev/null || \
|
||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peers WHERE is_deleted = 0" 2>/dev/null || \
|
||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peer WHERE is_deleted = 0" 2>/dev/null || echo "0")
|
||
online_count=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peers WHERE soft_deleted = 0 AND status = 'ONLINE'" 2>/dev/null || \
|
||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peers WHERE status = 1 AND is_deleted = 0" 2>/dev/null || \
|
||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM peer WHERE status = 1 AND is_deleted = 0" 2>/dev/null || echo "0")
|
||
user_count=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users" 2>/dev/null || echo "0")
|
||
|
||
echo -e " Database type: ${CYAN}SQLite${NC}"
|
||
echo -e " File: ${DIM}$DB_PATH${NC}"
|
||
echo " Devices: $device_count"
|
||
echo " Online: $online_count"
|
||
echo " Users: $user_count"
|
||
else
|
||
echo -e " Database type: ${CYAN}SQLite${NC} (configured)"
|
||
echo -e " ${YELLOW}SQLite database file not found: $DB_PATH${NC}"
|
||
echo -e " ${DIM}This is normal before the first device registers.${NC}"
|
||
fi
|
||
|
||
# --- Port diagnostics ---
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Port diagnostics ═══${NC}"
|
||
echo ""
|
||
|
||
local port_issues=0
|
||
local port_defs=(
|
||
"${GO_API_PORT:-21114}:TCP:betterdesk-serv|betterdesk-server|hbbs:Go HTTP API (handlers)"
|
||
"${CLIENT_API_PORT:-21121}:TCP:node|MainThread:Client API compat proxy → Go"
|
||
"21115:TCP:betterdesk-serv|betterdesk-server|hbbs:NAT Test"
|
||
"21116:TCP:betterdesk-serv|betterdesk-server|hbbs:ID Server (TCP)"
|
||
"21116:UDP:betterdesk-serv|betterdesk-server|hbbs:ID Server (UDP)"
|
||
"21117:TCP:betterdesk-serv|betterdesk-server|hbbr:Relay Server"
|
||
"5000:TCP:node|MainThread:Web Console"
|
||
)
|
||
|
||
for entry in "${port_defs[@]}"; do
|
||
IFS=':' read -r port proto expected desc <<< "$entry"
|
||
|
||
local listening=false
|
||
local proc_info=""
|
||
|
||
if [ "$proto" = "TCP" ]; then
|
||
proc_info=$(ss -tlnp 2>/dev/null | grep ":${port} " | head -1)
|
||
[ -z "$proc_info" ] && proc_info=$(netstat -tlnp 2>/dev/null | grep ":${port} " | head -1)
|
||
else
|
||
proc_info=$(ss -ulnp 2>/dev/null | grep ":${port} " | head -1)
|
||
[ -z "$proc_info" ] && proc_info=$(netstat -ulnp 2>/dev/null | grep ":${port} " | head -1)
|
||
fi
|
||
|
||
if [ -n "$proc_info" ]; then
|
||
listening=true
|
||
local process_name=$(echo "$proc_info" | grep -oP 'users:\(\("\K[^"]+' 2>/dev/null || \
|
||
echo "$proc_info" | awk '{print $NF}')
|
||
fi
|
||
|
||
printf " Port %s/%s (%-18s): " "$port" "$proto" "$desc"
|
||
|
||
if $listening; then
|
||
if [ -n "$process_name" ] && echo "$process_name" | grep -qiE "$expected"; then
|
||
echo -e "${GREEN}OK - $process_name${NC}"
|
||
elif [ -n "$process_name" ]; then
|
||
echo -e "${RED}CONFLICT - used by $process_name${NC}"
|
||
port_issues=$((port_issues + 1))
|
||
else
|
||
echo -e "${GREEN}LISTENING${NC}"
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}NOT LISTENING${NC}"
|
||
fi
|
||
done
|
||
|
||
if [ $port_issues -gt 0 ]; then
|
||
echo ""
|
||
print_warning "$port_issues port conflict(s) detected!"
|
||
echo -e " ${YELLOW}Tip: Stop conflicting processes or change ports in configuration${NC}"
|
||
fi
|
||
|
||
# --- Firewall diagnostics ---
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Firewall status ═══${NC}"
|
||
echo ""
|
||
|
||
local fw_type="none"
|
||
local missing_rules=0
|
||
local required_ports="${GO_API_PORT:-21114} ${CLIENT_API_PORT:-21121} 21115 21116 21117 5000"
|
||
|
||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then
|
||
fw_type="ufw"
|
||
echo -e " Firewall: ${YELLOW}UFW (active)${NC}"
|
||
echo ""
|
||
|
||
for port in $required_ports; do
|
||
local status_line=$(ufw status 2>/dev/null | grep -E "^${port}[/ ]")
|
||
printf " Port %-5s: " "$port"
|
||
if [ -n "$status_line" ]; then
|
||
echo -e "${GREEN}ALLOWED${NC}"
|
||
else
|
||
echo -e "${RED}NO RULE${NC}"
|
||
missing_rules=$((missing_rules + 1))
|
||
fi
|
||
done
|
||
|
||
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld 2>/dev/null; then
|
||
fw_type="firewalld"
|
||
echo -e " Firewall: ${YELLOW}firewalld (active)${NC}"
|
||
echo ""
|
||
|
||
local open_ports=$(firewall-cmd --list-ports 2>/dev/null)
|
||
for port in $required_ports; do
|
||
printf " Port %-5s: " "$port"
|
||
if echo "$open_ports" | grep -qE "${port}/tcp|${port}/udp"; then
|
||
echo -e "${GREEN}ALLOWED${NC}"
|
||
else
|
||
echo -e "${RED}NO RULE${NC}"
|
||
missing_rules=$((missing_rules + 1))
|
||
fi
|
||
done
|
||
|
||
elif iptables -L INPUT -n 2>/dev/null | grep -q "ACCEPT"; then
|
||
fw_type="iptables"
|
||
echo -e " Firewall: ${YELLOW}iptables${NC}"
|
||
echo ""
|
||
|
||
for port in $required_ports; do
|
||
printf " Port %-5s: " "$port"
|
||
if iptables -L INPUT -n 2>/dev/null | grep -qE "dpt:${port}\b"; then
|
||
echo -e "${GREEN}ALLOWED${NC}"
|
||
else
|
||
echo -e "${RED}NO RULE / CHECK MANUALLY${NC}"
|
||
missing_rules=$((missing_rules + 1))
|
||
fi
|
||
done
|
||
else
|
||
echo -e " Firewall: ${GREEN}No active firewall detected (all ports open)${NC}"
|
||
fi
|
||
|
||
if [ $missing_rules -gt 0 ]; then
|
||
echo ""
|
||
print_warning "$missing_rules firewall rule(s) missing!"
|
||
echo -e " ${YELLOW}Use option 'F' below to auto-configure firewall${NC}"
|
||
fi
|
||
|
||
# --- API connectivity test ---
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ API connectivity ═══${NC}"
|
||
echo ""
|
||
|
||
local api_port="${GO_API_PORT:-21114}"
|
||
|
||
# Detect if Go server API uses TLS (only if explicit --tls-api in service args)
|
||
local api_use_tls=false
|
||
local api_scheme="http"
|
||
if systemctl cat betterdesk-server.service 2>/dev/null | grep -qE '\-tls-api'; then
|
||
api_use_tls=true
|
||
api_scheme="https"
|
||
fi
|
||
|
||
printf " Go Server API (%s %s): " "$api_scheme" "$api_port"
|
||
if [ "$api_use_tls" = true ]; then
|
||
if curl -skfo /dev/null --connect-timeout 3 "https://127.0.0.1:${api_port}/api/health" 2>/dev/null; then
|
||
echo -e "${GREEN}OK (HTTPS)${NC}"
|
||
else
|
||
# Fallback: try HTTP in case TLS is only on signal/relay
|
||
if curl -sfo /dev/null --connect-timeout 3 "http://127.0.0.1:${api_port}/api/health" 2>/dev/null; then
|
||
echo -e "${GREEN}OK (HTTP)${NC}"
|
||
echo -e " ${YELLOW}⚠ Note: Go server has TLS cert but API responds on HTTP${NC}"
|
||
else
|
||
echo -e "${RED}UNREACHABLE${NC}"
|
||
echo -e " ${YELLOW}Tip: Check betterdesk-server logs: journalctl -u betterdesk-server -n 20${NC}"
|
||
fi
|
||
fi
|
||
else
|
||
if curl -sfo /dev/null --connect-timeout 3 "http://127.0.0.1:${api_port}/api/health" 2>/dev/null; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${RED}UNREACHABLE${NC}"
|
||
fi
|
||
fi
|
||
|
||
printf " Web Console (5000): "
|
||
if curl -sfo /dev/null --connect-timeout 3 "http://127.0.0.1:5000/health" 2>/dev/null; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${RED}UNREACHABLE${NC}"
|
||
fi
|
||
|
||
# --- TLS mismatch detection ---
|
||
if [ "$api_use_tls" = true ]; then
|
||
local console_api_url=""
|
||
# Check what URL the console is configured to use
|
||
if [ -f /etc/systemd/system/betterdesk-console.service ]; then
|
||
console_api_url=$(grep 'BETTERDESK_API_URL=' /etc/systemd/system/betterdesk-console.service 2>/dev/null | tail -1 | sed 's/.*BETTERDESK_API_URL=//')
|
||
fi
|
||
if [ -z "$console_api_url" ] && [ -f "$CONSOLE_PATH/.env" ]; then
|
||
console_api_url=$(grep -m1 '^BETTERDESK_API_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-)
|
||
fi
|
||
|
||
if [ -n "$console_api_url" ] && echo "$console_api_url" | grep -q '^http://'; then
|
||
echo ""
|
||
print_warning "TLS MISMATCH DETECTED!"
|
||
echo -e " ${YELLOW}Go server has TLS enabled but console is configured with HTTP:${NC}"
|
||
echo -e " ${YELLOW} Console URL: $console_api_url${NC}"
|
||
echo -e " ${YELLOW} Expected: https://localhost:$api_port/api${NC}"
|
||
echo -e " ${YELLOW} Fix: Re-run installation (option 1) or update .env and systemd service${NC}"
|
||
fi
|
||
fi
|
||
|
||
# --- Diagnostics sub-menu ---
|
||
echo ""
|
||
local _menu_items=(
|
||
$'Configure firewall rules\tAuto-create any missing firewall rules'
|
||
$'Test external ports\tCheck port connectivity from outside'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( F P 0 )
|
||
menu_choose "Diagnostics Actions" "Optional follow-up checks"
|
||
local sub_choice="$MENU_CHOICE"
|
||
|
||
case "$sub_choice" in
|
||
[Ff])
|
||
echo ""
|
||
configure_firewall_rules
|
||
press_enter
|
||
;;
|
||
[Pp])
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ External port test ═══${NC}"
|
||
echo ""
|
||
local server_ip=$(get_public_ip)
|
||
print_info "Public IP: $server_ip"
|
||
print_info "Testing external port accessibility..."
|
||
echo ""
|
||
|
||
for port in 21115 21116 21117; do
|
||
printf " Port %s: " "$port"
|
||
if timeout 3 bash -c "echo >/dev/tcp/$server_ip/$port" 2>/dev/null; then
|
||
echo -e "${GREEN}REACHABLE${NC}"
|
||
else
|
||
echo -e "${RED}BLOCKED/UNREACHABLE${NC}"
|
||
fi
|
||
done
|
||
press_enter
|
||
;;
|
||
*)
|
||
return
|
||
;;
|
||
esac
|
||
}
|
||
|
||
#===============================================================================
|
||
# Uninstall Functions
|
||
#===============================================================================
|
||
|
||
do_uninstall() {
|
||
print_header
|
||
echo -e "${RED}${BOLD}══════════ UNINSTALL ══════════${NC}"
|
||
echo ""
|
||
|
||
print_warning "This operation will remove BetterDesk Console!"
|
||
echo ""
|
||
|
||
if [ "$AUTO_MODE" != true ] && [ "$UNINSTALL_MODE" != true ]; then
|
||
if ! confirm "Are you sure you want to continue?"; then
|
||
return
|
||
fi
|
||
fi
|
||
|
||
if [ "$AUTO_MODE" = true ] || confirm "Create backup before uninstall?"; then
|
||
do_backup_silent
|
||
fi
|
||
|
||
print_step "Stopping services..."
|
||
# Stop Go server (primary)
|
||
systemctl stop betterdesk-server betterdesk-console 2>/dev/null || true
|
||
systemctl disable betterdesk-server betterdesk-console 2>/dev/null || true
|
||
# Stop legacy Rust services if they exist
|
||
systemctl stop rustdesksignal rustdeskrelay betterdesk betterdesk-api betterdesk-go 2>/dev/null || true
|
||
systemctl disable rustdesksignal rustdeskrelay betterdesk betterdesk-api betterdesk-go 2>/dev/null || true
|
||
|
||
print_step "Removing service files..."
|
||
# Remove Go services
|
||
rm -f /etc/systemd/system/betterdesk-server.service
|
||
rm -f /etc/systemd/system/betterdesk-console.service
|
||
# Remove legacy services
|
||
rm -f /etc/systemd/system/rustdesksignal.service
|
||
rm -f /etc/systemd/system/rustdeskrelay.service
|
||
rm -f /etc/systemd/system/betterdesk.service
|
||
rm -f /etc/systemd/system/betterdesk-api.service
|
||
rm -f /etc/systemd/system/betterdesk-go.service
|
||
systemctl daemon-reload
|
||
|
||
if [ "$PURGE_MODE" = true ] || { [ "$AUTO_MODE" != true ] && confirm "Remove installation files ($RUSTDESK_PATH)?"; }; then
|
||
rm -rf "$RUSTDESK_PATH"
|
||
print_info "Removed: $RUSTDESK_PATH"
|
||
else
|
||
print_info "Preserved server data: $RUSTDESK_PATH"
|
||
fi
|
||
|
||
if [ "$PURGE_MODE" = true ] || { [ "$AUTO_MODE" != true ] && confirm "Remove Web Console ($CONSOLE_PATH)?"; }; then
|
||
rm -rf "$CONSOLE_PATH"
|
||
print_info "Removed: $CONSOLE_PATH"
|
||
else
|
||
print_info "Preserved console data: $CONSOLE_PATH"
|
||
fi
|
||
|
||
print_success "BetterDesk has been uninstalled"
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# SSL Certificate Configuration
|
||
#===============================================================================
|
||
|
||
do_configure_ssl() {
|
||
maybe_reexec_if_installer_on_disk_is_newer
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ SSL CERTIFICATE CONFIGURATION ══════════${NC}"
|
||
echo ""
|
||
|
||
if [ ! -f "$CONSOLE_PATH/.env" ]; then
|
||
print_error "Node.js console .env not found at $CONSOLE_PATH/.env"
|
||
print_info "Please install BetterDesk first (option 1)"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local env_file="$CONSOLE_PATH/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local ssl_tls_active="no"
|
||
|
||
local _menu_items=(
|
||
$'Let\'s Encrypt\tAutomatic cert (needs domain name + port 80)'
|
||
$'Custom certificate\tProvide your own cert + key files'
|
||
$'Self-signed certificate\tLAN / testing only'
|
||
$'Disable SSL\tRevert the console to plain HTTP'
|
||
$'Enterprise TLS\tPanel + signal + relay TLS (API stays HTTP)'
|
||
$'External reverse proxy\tTLS at Caddy/Nginx — panel HTTP on localhost'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 5 6 )
|
||
menu_choose "SSL Certificate Configuration" "HTTPS for the panel, or TLS at an external reverse proxy"
|
||
local ssl_choice="$MENU_CHOICE"
|
||
|
||
case "${ssl_choice:-1}" in
|
||
1)
|
||
echo ""
|
||
read -p "Enter your domain name (e.g., betterdesk.example.com): " domain
|
||
if [ -z "$domain" ]; then
|
||
print_error "Domain name required for Let's Encrypt"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
if ! command -v certbot &> /dev/null; then
|
||
print_step "Installing certbot..."
|
||
if command -v apt-get &> /dev/null; then apt-get install -y certbot
|
||
elif command -v dnf &> /dev/null; then dnf install -y certbot
|
||
elif command -v yum &> /dev/null; then yum install -y certbot
|
||
elif command -v pacman &> /dev/null; then pacman -Sy --noconfirm certbot
|
||
else
|
||
print_error "Could not install certbot. Please install it manually."
|
||
press_enter
|
||
return
|
||
fi
|
||
fi
|
||
|
||
print_step "Requesting certificate for $domain..."
|
||
print_info "Port 80 must be accessible from the internet"
|
||
|
||
certbot certonly --standalone --preferred-challenges http \
|
||
-d "$domain" --non-interactive --agree-tos \
|
||
--email "admin@$domain" 2>&1 || {
|
||
print_error "Certificate request failed. Make sure port 80 is open and the domain points to this server."
|
||
press_enter
|
||
return
|
||
}
|
||
|
||
local le_live_dir="/etc/letsencrypt/live/$domain"
|
||
if ! deploy_ssl_material_to_rustdesk_dir \
|
||
"$le_live_dir/fullchain.pem" "$le_live_dir/privkey.pem" "$le_live_dir"; then
|
||
print_error "Failed to deploy Let's Encrypt certificate for console user"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
apply_console_protocol_mode https "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" true false
|
||
ssl_tls_active="yes"
|
||
|
||
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
|
||
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet") | crontab -
|
||
print_info "Auto-renewal cron job added (daily at 3:00 AM)"
|
||
fi
|
||
|
||
print_success "Let's Encrypt certificate configured for $domain"
|
||
print_info "Open the panel at https://${domain}:$(resolve_panel_https_port) (IP access will show certificate errors)"
|
||
;;
|
||
2)
|
||
echo ""
|
||
read -p "Path to certificate file (PEM): " cert_path
|
||
read -p "Path to private key file (PEM): " key_path
|
||
read -p "Path to CA bundle (optional, press Enter to skip): " ca_path
|
||
|
||
if [ ! -f "$cert_path" ] || [ ! -f "$key_path" ]; then
|
||
print_error "Certificate or key file not found."
|
||
press_enter
|
||
return
|
||
fi
|
||
if ! openssl x509 -in "$cert_path" -noout 2>/dev/null; then
|
||
print_error "The provided certificate is not a valid X.509 file."
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
local deploy_crt="$cert_path" merged_crt=""
|
||
if [ -n "$ca_path" ] && [ -f "$ca_path" ]; then
|
||
merged_crt=$(mktemp)
|
||
cat "$cert_path" "$ca_path" > "$merged_crt"
|
||
deploy_crt="$merged_crt"
|
||
fi
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$deploy_crt" "$key_path"; then
|
||
[ -n "$merged_crt" ] && rm -f "$merged_crt"
|
||
print_error "Failed to deploy custom certificate for console user"
|
||
press_enter
|
||
return
|
||
fi
|
||
[ -n "$merged_crt" ] && rm -f "$merged_crt"
|
||
|
||
infer_tls_mode_from_cert "$ssl_dir/betterdesk.crt"
|
||
apply_console_protocol_mode https "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" \
|
||
"$INFERRED_RUSTDESK_API_TLS" "$INFERRED_ALLOW_SELF_SIGNED"
|
||
if [ -n "$ca_path" ] && [ -f "$ca_path" ]; then
|
||
_upsert_env_line "$env_file" SSL_CA_PATH "$ca_path"
|
||
fi
|
||
ssl_tls_active="yes"
|
||
print_success "Custom SSL certificate configured"
|
||
;;
|
||
3)
|
||
mkdir -p "$ssl_dir"
|
||
echo ""
|
||
read -p "Enter domain name (optional, press Enter to skip): " cert_domain
|
||
|
||
local server_ip lan_ip san_list cn
|
||
server_ip=$(get_public_ip)
|
||
lan_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1 2>/dev/null || \
|
||
hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||
san_list="IP:$server_ip,IP:127.0.0.1,DNS:localhost"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && san_list="$san_list,IP:$lan_ip"
|
||
[ -n "$cert_domain" ] && san_list="DNS:$cert_domain,$san_list"
|
||
cn="${cert_domain:-$server_ip}"
|
||
|
||
print_step "Generating self-signed certificate..."
|
||
print_info "SANs: $san_list"
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk/C=PL" \
|
||
-addext "subjectAltName=$san_list" 2>/dev/null || \
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk/C=PL" 2>/dev/null
|
||
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key"; then
|
||
print_error "Failed to set permissions on self-signed certificate"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
apply_console_protocol_mode https "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" false true
|
||
ssl_tls_active="yes"
|
||
print_success "Self-signed certificate generated (valid 10 years)"
|
||
print_info "Certificate: $ssl_dir/betterdesk.crt"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && print_info "LAN IP included: $lan_ip"
|
||
print_warning "Browsers will show security warning. Use Let's Encrypt for public servers."
|
||
;;
|
||
4)
|
||
apply_console_protocol_mode http
|
||
clear_go_server_signal_relay_tls
|
||
print_success "SSL disabled. Running in HTTP mode."
|
||
print_info "If your browser still redirects to HTTPS, clear site cache or HSTS (chrome://net-internals/#hsts)"
|
||
;;
|
||
5)
|
||
print_header "Enterprise TLS Configuration"
|
||
echo ""
|
||
print_warning "IMPORTANT: Go API port ${GO_API_PORT:-21114} stays HTTP for RustDesk client compatibility."
|
||
echo ""
|
||
|
||
mkdir -p "$ssl_dir"
|
||
read -p "Enter domain name (optional, press Enter to skip): " cert_domain
|
||
|
||
local server_ip lan_ip san_list cn
|
||
server_ip=$(get_public_ip)
|
||
lan_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1 2>/dev/null || \
|
||
hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||
san_list="IP:$server_ip,IP:127.0.0.1,DNS:localhost"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && san_list="$san_list,IP:$lan_ip"
|
||
[ -n "$cert_domain" ] && san_list="DNS:$cert_domain,$san_list"
|
||
cn="${cert_domain:-$server_ip}"
|
||
|
||
print_step "Generating Enterprise certificate..."
|
||
print_info "SANs: $san_list"
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk Enterprise/C=PL" \
|
||
-addext "subjectAltName=$san_list" 2>/dev/null || \
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=$cn/O=BetterDesk Enterprise/C=PL" 2>/dev/null
|
||
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key"; then
|
||
print_error "Failed to set permissions on Enterprise certificate"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
apply_console_protocol_mode https "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" true true
|
||
_upsert_env_line "$env_file" ENTERPRISE_TLS true
|
||
if [ -f "$svc_file" ]; then
|
||
_upsert_systemd_env "$svc_file" ENTERPRISE_TLS true
|
||
fi
|
||
ssl_tls_active="yes"
|
||
|
||
print_success "Enterprise TLS configured successfully!"
|
||
echo ""
|
||
print_info "Certificate: $ssl_dir/betterdesk.crt"
|
||
print_info "Private key: $ssl_dir/betterdesk.key"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && print_info "LAN IP: $lan_ip"
|
||
echo ""
|
||
print_info " Panel HTTPS: :$(resolve_panel_https_port)"
|
||
print_info " Signal TLS: :21116"
|
||
print_info " Relay TLS: :21117"
|
||
print_info " Go API HTTP: :${GO_API_PORT:-21114} (RustDesk client compatibility)"
|
||
;;
|
||
6)
|
||
do_configure_reverse_proxy || true
|
||
;;
|
||
*)
|
||
print_warning "Invalid option"
|
||
press_enter
|
||
return
|
||
;;
|
||
esac
|
||
|
||
if [ "$ssl_tls_active" = "yes" ]; then
|
||
sync_go_server_signal_relay_tls "$ssl_dir"
|
||
ensure_betterdesk_console_user >/dev/null
|
||
print_info "Signal/relay TLS enabled; Go API stays HTTP (RustDesk client compatibility)"
|
||
maybe_offer_standard_https_port
|
||
fi
|
||
|
||
echo ""
|
||
if confirm "Restart BetterDesk to apply changes?"; then
|
||
repair_https_stuck_state yes
|
||
ensure_console_tls_material_readable 2>/dev/null || true
|
||
systemctl restart betterdesk-server betterdesk-console 2>/dev/null || true
|
||
print_success "BetterDesk services restarted"
|
||
verify_service_health "betterdesk-server" "21116" 15 >/dev/null 2>&1 || true
|
||
verify_service_health "betterdesk-console" "$(resolve_panel_health_port)" 15 >/dev/null 2>&1 || true
|
||
run_protocol_tests
|
||
fi
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Protocol verification test-suite
|
||
#===============================================================================
|
||
# Runs a series of non-destructive connectivity / certificate checks after an
|
||
# HTTP <-> HTTPS switch so the operator gets immediate, trustworthy feedback.
|
||
# Honours the project invariant: the Go API (:21121) must remain HTTP for RustDesk clients.
|
||
run_protocol_tests() {
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
local pass=0 fail=0 warn=0
|
||
|
||
echo ""
|
||
echo -e "${WHITE}${BOLD}═══ Post-configuration tests ═══${NC}"
|
||
echo ""
|
||
|
||
_test_ok() { echo -e " ${GREEN}✓${NC} $1"; pass=$((pass+1)); }
|
||
_test_fail() { echo -e " ${RED}✗${NC} $1"; fail=$((fail+1)); }
|
||
_test_warn() { echo -e " ${YELLOW}!${NC} $1"; warn=$((warn+1)); }
|
||
|
||
# ── 1. Services running ──
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
_test_ok "Go server service is active"
|
||
else
|
||
_test_fail "Go server service is NOT active (journalctl -u betterdesk-server)"
|
||
if journalctl -u betterdesk-server --no-pager -n 40 2>/dev/null | grep -q 'listen tcp :5000'; then
|
||
echo -e " ${DIM}Hint: Go tried signal on :5000 — panel PORT in .env leaked; run Repair → Repair HTTPS/TLS (#219)${NC}"
|
||
fi
|
||
fi
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null; then
|
||
_test_ok "Web console service is active"
|
||
else
|
||
_test_fail "Web console service is NOT active (journalctl -u betterdesk-console)"
|
||
fi
|
||
|
||
# ── 1b. Wait for Go signal port (post-restart boot delay) ──
|
||
if systemctl is-active --quiet betterdesk-server 2>/dev/null; then
|
||
if verify_service_health "betterdesk-server" "21116" 15 >/dev/null 2>&1; then
|
||
_test_ok "Go server listening on signal port :21116"
|
||
elif journalctl -u betterdesk-server --no-pager -n 40 2>/dev/null | grep -q 'listen tcp :5000'; then
|
||
_test_fail "Go server tried signal on :5000 (conflicts with panel redirect) — Repair → Repair HTTPS/TLS (#219)"
|
||
else
|
||
_test_fail "Go server not listening on :21116 yet (journalctl -u betterdesk-server)"
|
||
fi
|
||
fi
|
||
|
||
# ── 2. Effective runtime configuration (systemd overrides .env) ──
|
||
local https_enabled http_port https_port http_redirect api_tls_mode
|
||
https_enabled=$(read_effective_console_setting HTTPS_ENABLED false)
|
||
http_port=$(resolve_panel_http_port)
|
||
https_port=$(resolve_panel_https_port)
|
||
http_redirect=$(read_effective_console_setting HTTP_REDIRECT_HTTPS true)
|
||
api_tls_mode=$(read_effective_console_setting RUSTDESK_API_TLS auto)
|
||
|
||
local panel_scheme="http" panel_port
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
panel_scheme="https"
|
||
panel_port="$https_port"
|
||
else
|
||
panel_port="$http_port"
|
||
fi
|
||
|
||
# ── 2c. External reverse-proxy mode (TRUST_PROXY + plain HTTP panel) ──
|
||
local trust_proxy_val host_bind
|
||
trust_proxy_val=$(read_effective_console_setting TRUST_PROXY false)
|
||
host_bind=$(read_effective_console_setting HOST "127.0.0.1")
|
||
local _trust_on="no"
|
||
case "$(echo "$trust_proxy_val" | tr '[:upper:]' '[:lower:]')" in
|
||
y|yes|1|true|on) _trust_on="yes" ;;
|
||
esac
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" != "true" ] && [ "$_trust_on" = "yes" ]; then
|
||
if [ "$host_bind" = "127.0.0.1" ] || [ "$host_bind" = "localhost" ]; then
|
||
_test_ok "Reverse-proxy mode: panel bound to localhost ($host_bind)"
|
||
elif [ "$host_bind" = "0.0.0.0" ]; then
|
||
_test_ok "Reverse-proxy mode: panel bound to all interfaces (remote proxy host)"
|
||
else
|
||
_test_warn "TRUST_PROXY enabled with HOST=$host_bind (use 127.0.0.1 same-host or 0.0.0.0 remote proxy)"
|
||
fi
|
||
if [ -f "$go_svc_file" ] && grep -qE 'Environment=TRUST_PROXY=Y|-trust-proxy' "$go_svc_file" 2>/dev/null; then
|
||
_test_ok "Go server trusts reverse-proxy headers (TRUST_PROXY / -trust-proxy)"
|
||
if grep -qE 'Environment=TRUSTED_PROXIES=.+' "$go_svc_file" 2>/dev/null || \
|
||
grep -qE '^TRUSTED_PROXIES=.+' "${CONSOLE_PATH}/.env" 2>/dev/null; then
|
||
_test_ok "TRUSTED_PROXIES allowlist configured (#276)"
|
||
else
|
||
_test_warn "TRUSTED_PROXIES empty — Go ignores X-Forwarded-* until set (e.g. 127.0.0.1/32)"
|
||
fi
|
||
else
|
||
_test_fail "Go server TRUST_PROXY not enabled — API rate limits may use proxy IP only"
|
||
fi
|
||
local rp_dir="$RUSTDESK_PATH/reverse-proxy"
|
||
if [ -d "$rp_dir" ] && { [ -f "$rp_dir/caddy.Caddyfile.snippet" ] || [ -f "$rp_dir/nginx.betterdesk.conf.snippet" ]; }; then
|
||
_test_ok "Reverse-proxy snippets in $rp_dir/"
|
||
else
|
||
_test_warn "No snippets in $rp_dir/ — re-run SSL menu → External reverse proxy"
|
||
fi
|
||
fi
|
||
|
||
# ── 2b. TLS key readable by console user (HTTPS only) ──
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
local ssl_key_path console_user="betterdesk"
|
||
ssl_key_path=$(read_effective_console_setting SSL_KEY_PATH "")
|
||
if id "$console_user" &>/dev/null && [ -n "$ssl_key_path" ] && [ -e "$ssl_key_path" ]; then
|
||
if runuser -u "$console_user" -- test -r "$ssl_key_path" 2>/dev/null; then
|
||
_test_ok "TLS private key readable by console user ($console_user)"
|
||
else
|
||
_test_fail "Console user $console_user cannot read TLS key ($ssl_key_path) — HTTPS panel will fall back to HTTP (journalctl -u betterdesk-console)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── 3. Panel reachability on the correct scheme/port ──
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null; then
|
||
verify_service_health "betterdesk-console" "$panel_port" 15 >/dev/null 2>&1 || true
|
||
fi
|
||
local panel_code panel_insecure="no"
|
||
[ "$panel_scheme" = "https" ] && panel_insecure="yes"
|
||
panel_code=$(_wait_for_http_code "${panel_scheme}://127.0.0.1:${panel_port}/" 15 "$panel_insecure" || true)
|
||
if [[ "$panel_code" =~ ^(200|301|302|304|401|403)$ ]]; then
|
||
_test_ok "Web panel reachable: ${panel_scheme}://<server>:${panel_port} (HTTP $panel_code)"
|
||
else
|
||
local alt_panel_port=""
|
||
if [ "$panel_scheme" = "https" ] && [ "$panel_port" = "443" ] && _tcp_port_is_listening 5443; then
|
||
alt_panel_port="5443"
|
||
panel_code=$(_wait_for_http_code "https://127.0.0.1:5443/" 5 "$panel_insecure" || true)
|
||
if [[ "$panel_code" =~ ^(200|301|302|304|401|403)$ ]]; then
|
||
_test_fail "Web panel NOT reachable on https://127.0.0.1:443 (panel bound :5443 instead — run Repair → Repair permissions for CAP_NET_BIND_SERVICE)"
|
||
else
|
||
_test_fail "Web panel NOT reachable on ${panel_scheme}://127.0.0.1:${panel_port} (got $panel_code)"
|
||
fi
|
||
else
|
||
_test_fail "Web panel NOT reachable on ${panel_scheme}://127.0.0.1:${panel_port} (got $panel_code)"
|
||
fi
|
||
if systemctl is-active --quiet betterdesk-console 2>/dev/null && [ "$panel_scheme" = "https" ]; then
|
||
if journalctl -u betterdesk-console --no-pager -n 80 2>/dev/null | grep -qi 'Falling back to HTTP'; then
|
||
echo -e " ${DIM}Hint: console logged HTTPS fallback — check TLS key permissions (runuser -u betterdesk test -r key)${NC}"
|
||
elif [ -n "$alt_panel_port" ]; then
|
||
echo -e " ${DIM}Hint: configured HTTPS_PORT=443 but Node bound :5443 — Repair → Repair permissions, then restart (#219)${NC}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── 3b. HTTP→HTTPS redirect (only when HTTPS + redirect enabled) ──
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ] \
|
||
&& [ "$(echo "$http_redirect" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
local redirect_hdr _r_elapsed=0 redirect_probe_port="$http_port"
|
||
# Standard HTTPS on :443 always redirects from :80 — never probe stale :5000 (#219).
|
||
if [ "$https_port" = "443" ]; then
|
||
redirect_probe_port="80"
|
||
fi
|
||
redirect_hdr=""
|
||
while [ "$_r_elapsed" -lt 10 ]; do
|
||
redirect_hdr=$(curl -sI --max-time 4 "http://127.0.0.1:${redirect_probe_port}/" 2>/dev/null | grep -i '^location:' | head -1)
|
||
if [ "$https_port" = "443" ]; then
|
||
if echo "$redirect_hdr" | grep -qi 'https://' \
|
||
&& { ! echo "$redirect_hdr" | grep -qiE ':[0-9]+' || echo "$redirect_hdr" | grep -qi ':443'; }; then
|
||
break
|
||
fi
|
||
elif echo "$redirect_hdr" | grep -qi ":${https_port}"; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
_r_elapsed=$((_r_elapsed + 1))
|
||
done
|
||
if [ "$https_port" = "443" ]; then
|
||
if echo "$redirect_hdr" | grep -qi 'https://' \
|
||
&& { ! echo "$redirect_hdr" | grep -qiE ':[0-9]+' || echo "$redirect_hdr" | grep -qi ':443'; }; then
|
||
_test_ok "HTTP redirect active: :${redirect_probe_port} → HTTPS :${https_port}"
|
||
else
|
||
_test_fail "HTTP redirect missing or wrong target on :${redirect_probe_port} (got: ${redirect_hdr:-none})"
|
||
if [ "$redirect_probe_port" = "80" ]; then
|
||
echo -e " ${DIM}Hint: set PORT=80 in .env, run Repair → Repair HTTPS/TLS, ensure CAP_NET_BIND_SERVICE (#219)${NC}"
|
||
fi
|
||
fi
|
||
elif echo "$redirect_hdr" | grep -qi ":${https_port}"; then
|
||
_test_ok "HTTP redirect active: :${http_port} → HTTPS :${https_port}"
|
||
else
|
||
_test_fail "HTTP redirect missing or wrong target on :${http_port} (got: ${redirect_hdr:-none})"
|
||
fi
|
||
fi
|
||
|
||
# ── 4. Go API (RustDesk client + REST) on GO_API_PORT (default 21114) ──
|
||
local go_api_port="${GO_API_PORT:-21114}"
|
||
local client_api_port="${CLIENT_API_PORT:-21121}"
|
||
local api_code _api_elapsed=0
|
||
while [ "$_api_elapsed" -lt 15 ]; do
|
||
api_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 4 \
|
||
"http://127.0.0.1:${go_api_port}/api/server/stats" 2>/dev/null || echo "000")
|
||
if [[ "$api_code" =~ ^(200|401|403|404)$ ]]; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
_api_elapsed=$((_api_elapsed + 1))
|
||
done
|
||
if [[ "$api_code" =~ ^(200|401|403|404)$ ]]; then
|
||
_test_ok "Go API responding over HTTP on :${go_api_port} (HTTP $api_code)"
|
||
else
|
||
_test_fail "Go API not responding over HTTP on :${go_api_port} (got $api_code)"
|
||
if ! ss -tlnH 2>/dev/null | grep -q ":${go_api_port} "; then
|
||
if ss -tlnpH 2>/dev/null | grep ":${client_api_port} " | grep -qiE 'betterdesk-server|betterdesk-serv'; then
|
||
echo -e " ${DIM}Hint: Go API bound to :${client_api_port} — API_PORT from .env leaked; run Repair → Repair HTTPS/TLS or update (#219)${NC}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── 4b. Client API compat proxy (:21121) — scheme matches RUSTDESK_API_TLS ──
|
||
local client_api_scheme="http" client_api_code client_insecure="no"
|
||
if client_api_should_use_tls; then
|
||
client_api_scheme="https"
|
||
client_insecure="yes"
|
||
fi
|
||
client_api_code=$(_wait_for_http_code \
|
||
"${client_api_scheme}://127.0.0.1:${CLIENT_API_PORT:-21121}/api/login-options" 15 "$client_insecure" || true)
|
||
if [[ "$client_api_code" =~ ^(200|401|403|404|405)$ ]]; then
|
||
_test_ok "Client API compat proxy on :${CLIENT_API_PORT:-21121} (${client_api_scheme^^} $client_api_code)"
|
||
else
|
||
_test_fail "Client API proxy not responding on :${CLIENT_API_PORT:-21121} (${client_api_scheme}, got $client_api_code) — check API_ENABLED / RUSTDESK_API_TLS"
|
||
fi
|
||
|
||
# Critical invariant: Go API must never be HTTPS-only
|
||
if [ -f "$go_svc_file" ] && grep -Eq '\-tls-api|\-force-https' "$go_svc_file" 2>/dev/null; then
|
||
_test_warn "Go service carries -tls-api/-force-https — RustDesk clients require plain HTTP on :${GO_API_PORT:-21114}"
|
||
fi
|
||
|
||
# ── 5. Signal / Relay listeners ──
|
||
local p
|
||
for p in 21116 21117; do
|
||
if ss -tlnH 2>/dev/null | grep -q ":${p} "; then
|
||
_test_ok "Listener present on TCP :${p}"
|
||
else
|
||
_test_warn "No TCP listener detected on :${p} (UDP-only signal is normal for :21116)"
|
||
fi
|
||
done
|
||
|
||
# ── 6. Certificate validation (HTTPS / TLS modes) ──
|
||
local tls_active="no"
|
||
[ -f "$go_svc_file" ] && grep -q '\-tls-signal' "$go_svc_file" 2>/dev/null && tls_active="yes"
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ] || [ "$tls_active" = "yes" ]; then
|
||
if [ -f "$ssl_dir/betterdesk.crt" ]; then
|
||
if openssl x509 -in "$ssl_dir/betterdesk.crt" -noout 2>/dev/null; then
|
||
local not_after days_left
|
||
not_after=$(openssl x509 -in "$ssl_dir/betterdesk.crt" -noout -enddate 2>/dev/null | cut -d= -f2)
|
||
if [ -n "$not_after" ]; then
|
||
local exp_epoch now_epoch
|
||
exp_epoch=$(date -d "$not_after" +%s 2>/dev/null || echo 0)
|
||
now_epoch=$(date +%s)
|
||
if [ "$exp_epoch" -gt "$now_epoch" ]; then
|
||
days_left=$(( (exp_epoch - now_epoch) / 86400 ))
|
||
if [ "$days_left" -lt 14 ]; then
|
||
_test_warn "Certificate valid but expires in ${days_left} days ($not_after)"
|
||
else
|
||
_test_ok "Certificate valid for ${days_left} more days (until $not_after)"
|
||
fi
|
||
else
|
||
_test_fail "Certificate has EXPIRED ($not_after)"
|
||
fi
|
||
fi
|
||
local san
|
||
san=$(openssl x509 -in "$ssl_dir/betterdesk.crt" -noout -ext subjectAltName 2>/dev/null | tail -n +2 | tr -d ' ')
|
||
[ -n "$san" ] && echo -e " ${DIM}SAN: ${san}${NC}"
|
||
else
|
||
_test_fail "Certificate file is not a valid X.509 certificate"
|
||
fi
|
||
else
|
||
_test_fail "HTTPS/TLS enabled but no certificate found at $ssl_dir/betterdesk.crt"
|
||
fi
|
||
|
||
if [ "$tls_active" = "yes" ]; then
|
||
if echo | timeout 5 openssl s_client -connect "127.0.0.1:21116" 2>/dev/null | grep -q 'BEGIN CERTIFICATE'; then
|
||
_test_ok "TLS handshake succeeded on signal :21116"
|
||
else
|
||
_test_warn "Could not complete TLS handshake on :21116 (dual-mode listener may still accept plain TCP)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
local go_signal_port relay_port
|
||
go_signal_port=$(grep -m1 '^Environment=SIGNAL_PORT=' "$go_svc_file" 2>/dev/null | cut -d= -f2- || echo "21116")
|
||
relay_port=$(grep -m1 '^Environment=RELAY_PORT=' "$go_svc_file" 2>/dev/null | cut -d= -f2- || echo "21117")
|
||
echo -e " ${DIM}Effective config: HTTPS_ENABLED=${https_enabled} panel=${panel_scheme}:${panel_port} redirect=${http_redirect} client_api_tls=${api_tls_mode} go_signal=${go_signal_port} go_relay=${relay_port}${NC}"
|
||
echo ""
|
||
echo -e " ${GREEN}${pass} passed${NC} ${YELLOW}${warn} warnings${NC} ${RED}${fail} failed${NC}"
|
||
if [ "$fail" -gt 0 ]; then
|
||
echo -e " ${YELLOW}Some checks failed — review the messages above and the service logs.${NC}"
|
||
else
|
||
echo -e " ${GREEN}Configuration verified successfully.${NC}"
|
||
if [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" = "true" ] && [ "$https_port" = "5443" ]; then
|
||
echo ""
|
||
echo -e " ${DIM}Tip: for https://your-domain without :5443, set HTTPS_PORT=443 in .env and run Repair → Repair permissions, or re-run Protocol Toggle / SSL config and choose standard port 443. See docs/setup/HTTPS_SETUP.md${NC}"
|
||
elif [ "$(echo "$https_enabled" | tr '[:upper:]' '[:lower:]')" != "true" ] && [ "$_trust_on" = "yes" ]; then
|
||
echo ""
|
||
echo -e " ${DIM}Tip: configure Caddy/Nginx using $RUSTDESK_PATH/reverse-proxy/ snippets, then open https://your-domain/ (not :5443). See docs/setup/REVERSE_PROXY.md${NC}"
|
||
fi
|
||
fi
|
||
echo ""
|
||
|
||
unset -f _test_ok _test_fail _test_warn 2>/dev/null || true
|
||
}
|
||
|
||
#===============================================================================
|
||
# HTTP/HTTPS Protocol Toggle
|
||
#===============================================================================
|
||
|
||
do_toggle_protocol() {
|
||
maybe_reexec_if_installer_on_disk_is_newer
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ PROTOCOL TOGGLE (HTTP / HTTPS) ══════════${NC}"
|
||
echo ""
|
||
|
||
local env_file="$CONSOLE_PATH/.env"
|
||
local svc_file="/etc/systemd/system/betterdesk-console.service"
|
||
local go_svc_file="/etc/systemd/system/betterdesk-server.service"
|
||
local ssl_dir="$RUSTDESK_PATH/ssl"
|
||
|
||
# Detect current mode (effective runtime: systemd overrides .env)
|
||
local current_mode="HTTP"
|
||
if [ "$(read_effective_console_setting HTTPS_ENABLED false | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||
current_mode="HTTPS"
|
||
fi
|
||
|
||
local tls_signal="no"
|
||
local tls_relay="no"
|
||
if [ -f "$go_svc_file" ]; then
|
||
grep -q '\-tls-signal' "$go_svc_file" 2>/dev/null && tls_signal="yes"
|
||
grep -q '\-tls-relay' "$go_svc_file" 2>/dev/null && tls_relay="yes"
|
||
fi
|
||
|
||
local _menu_items=(
|
||
$'Switch to HTTP\tEverything plain — LAN / testing'
|
||
$'Switch to HTTPS\tPanel HTTPS + signal/relay TLS'
|
||
$'External reverse proxy\tTLS at Caddy/Nginx — panel HTTP on localhost'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 0 )
|
||
menu_choose "Protocol Toggle (HTTP / HTTPS / reverse proxy)" "Current: ${current_mode} | signal TLS: ${tls_signal} | relay TLS: ${tls_relay}"
|
||
local proto_choice="$MENU_CHOICE"
|
||
|
||
case "${proto_choice:-0}" in
|
||
1)
|
||
# ── Switch to HTTP ──
|
||
echo ""
|
||
print_step "Switching to HTTP mode..."
|
||
|
||
apply_console_protocol_mode http
|
||
clear_go_server_signal_relay_tls
|
||
|
||
print_success "Switched to HTTP mode"
|
||
echo ""
|
||
print_info " Panel: HTTP :$(resolve_panel_http_port)"
|
||
print_info " Signal: TCP :21116"
|
||
print_info " Relay: TCP :21117"
|
||
print_info " Go API: HTTP :${GO_API_PORT:-21114}"
|
||
print_info " Client API: HTTP :${CLIENT_API_PORT:-21121}"
|
||
echo ""
|
||
print_warning "SSL certificates were NOT deleted (use option C > 4 to remove)"
|
||
print_info "If your browser still redirects to HTTPS, clear site cache or HSTS (chrome://net-internals/#hsts)"
|
||
;;
|
||
2)
|
||
# ── Switch to HTTPS ──
|
||
echo ""
|
||
local have_cert="no"
|
||
[ -f "$ssl_dir/betterdesk.crt" ] && [ -f "$ssl_dir/betterdesk.key" ] && have_cert="yes"
|
||
local _keep_desc="No existing certificate found"
|
||
[ "$have_cert" = "yes" ] && _keep_desc="Reuse $ssl_dir/betterdesk.crt"
|
||
local _menu_items=(
|
||
$'Keep existing certificate\t'"$_keep_desc"
|
||
$'Self-signed certificate\tGenerate one for LAN / testing'
|
||
$'Let\'s Encrypt certificate\tPublic domain, port 80 must be free'
|
||
$'Custom certificate\tPaste your own cert + key file paths'
|
||
$'Cancel\tDo not change the protocol'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 0 )
|
||
menu_choose "HTTPS Certificate Source" "Choose the certificate to use for TLS"
|
||
local cert_choice="$MENU_CHOICE"
|
||
|
||
case "${cert_choice:-2}" in
|
||
1)
|
||
if [ "$have_cert" != "yes" ]; then
|
||
print_error "No existing certificate found — choose another option."
|
||
press_enter
|
||
return
|
||
fi
|
||
maybe_repair_le_ssl_symlinks 2>/dev/null || true
|
||
print_info "Using existing certificate at $ssl_dir/betterdesk.crt"
|
||
;;
|
||
2)
|
||
mkdir -p "$ssl_dir"
|
||
local server_ip lan_ip san_list
|
||
server_ip=$(get_public_ip 2>/dev/null || echo "127.0.0.1")
|
||
lan_ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
||
san_list="IP:$server_ip,IP:127.0.0.1,DNS:localhost"
|
||
[ -n "$lan_ip" ] && [ "$lan_ip" != "$server_ip" ] && san_list="$san_list,IP:$lan_ip"
|
||
read -p "Optional DNS domain for the certificate (blank to skip): " ss_domain
|
||
[ -n "$ss_domain" ] && san_list="$san_list,DNS:$ss_domain"
|
||
print_step "Generating self-signed certificate..."
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=${ss_domain:-$server_ip}/O=BetterDesk/C=PL" \
|
||
-addext "subjectAltName=$san_list" 2>/dev/null || \
|
||
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
|
||
-keyout "$ssl_dir/betterdesk.key" \
|
||
-out "$ssl_dir/betterdesk.crt" \
|
||
-subj "/CN=${ss_domain:-$server_ip}/O=BetterDesk/C=PL" 2>/dev/null
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key"; then
|
||
print_error "Failed to set permissions on self-signed certificate"
|
||
press_enter
|
||
return
|
||
fi
|
||
print_success "Self-signed certificate generated"
|
||
;;
|
||
3)
|
||
if ! command -v certbot &>/dev/null; then
|
||
print_step "Installing certbot..."
|
||
if command -v dnf &>/dev/null; then dnf install -y certbot &>/dev/null
|
||
elif command -v apt-get &>/dev/null; then apt-get install -y certbot &>/dev/null
|
||
elif command -v yum &>/dev/null; then yum install -y certbot &>/dev/null; fi
|
||
fi
|
||
if ! command -v certbot &>/dev/null; then
|
||
print_error "certbot could not be installed automatically."
|
||
press_enter
|
||
return
|
||
fi
|
||
read -p "Public domain (e.g. desk.example.com): " le_domain
|
||
read -p "Admin email (for renewal notices): " le_email
|
||
if [ -z "$le_domain" ]; then
|
||
print_error "A domain is required for Let's Encrypt."
|
||
press_enter
|
||
return
|
||
fi
|
||
print_step "Requesting certificate for $le_domain (standalone, needs port 80)..."
|
||
if certbot certonly --standalone --non-interactive --agree-tos \
|
||
${le_email:+--email "$le_email"} ${le_email:+} \
|
||
$([ -z "$le_email" ] && echo "--register-unsafely-without-email") \
|
||
-d "$le_domain"; then
|
||
local le_live_dir="/etc/letsencrypt/live/$le_domain"
|
||
if ! deploy_ssl_material_to_rustdesk_dir \
|
||
"$le_live_dir/fullchain.pem" "$le_live_dir/privkey.pem" "$le_live_dir"; then
|
||
print_error "Failed to deploy Let's Encrypt certificate for console user"
|
||
press_enter
|
||
return
|
||
fi
|
||
print_success "Let's Encrypt certificate installed for $le_domain"
|
||
print_info "Open the panel at https://${le_domain}:$(resolve_panel_https_port) (IP access will show certificate errors)"
|
||
else
|
||
print_error "certbot failed — check that DNS points here and port 80 is free."
|
||
press_enter
|
||
return
|
||
fi
|
||
;;
|
||
4)
|
||
read -p "Path to certificate (.crt/.pem, fullchain): " custom_crt
|
||
read -p "Path to private key (.key): " custom_key
|
||
read -p "Path to CA chain (optional, blank to skip): " custom_ca
|
||
if [ ! -f "$custom_crt" ] || [ ! -f "$custom_key" ]; then
|
||
print_error "Certificate or key file not found."
|
||
press_enter
|
||
return
|
||
fi
|
||
if ! openssl x509 -in "$custom_crt" -noout 2>/dev/null; then
|
||
print_error "The provided certificate is not a valid X.509 file."
|
||
press_enter
|
||
return
|
||
fi
|
||
local deploy_crt="$custom_crt" merged_crt=""
|
||
if [ -n "$custom_ca" ] && [ -f "$custom_ca" ]; then
|
||
merged_crt=$(mktemp)
|
||
cat "$custom_crt" "$custom_ca" > "$merged_crt"
|
||
deploy_crt="$merged_crt"
|
||
fi
|
||
if ! deploy_ssl_material_to_rustdesk_dir "$deploy_crt" "$custom_key"; then
|
||
[ -n "$merged_crt" ] && rm -f "$merged_crt"
|
||
print_error "Failed to deploy custom certificate for console user"
|
||
press_enter
|
||
return
|
||
fi
|
||
[ -n "$merged_crt" ] && rm -f "$merged_crt"
|
||
print_success "Custom certificate installed"
|
||
;;
|
||
0|*)
|
||
print_info "Cancelled — no changes made."
|
||
press_enter
|
||
return
|
||
;;
|
||
esac
|
||
|
||
local api_tls="false" allow_self_signed="true"
|
||
case "${cert_choice:-2}" in
|
||
1)
|
||
infer_tls_mode_from_cert "$ssl_dir/betterdesk.crt"
|
||
api_tls="$INFERRED_RUSTDESK_API_TLS"
|
||
allow_self_signed="$INFERRED_ALLOW_SELF_SIGNED"
|
||
;;
|
||
2)
|
||
api_tls="false"
|
||
allow_self_signed="true"
|
||
;;
|
||
3)
|
||
api_tls="true"
|
||
allow_self_signed="false"
|
||
;;
|
||
4)
|
||
infer_tls_mode_from_cert "$ssl_dir/betterdesk.crt"
|
||
api_tls="$INFERRED_RUSTDESK_API_TLS"
|
||
allow_self_signed="$INFERRED_ALLOW_SELF_SIGNED"
|
||
;;
|
||
esac
|
||
|
||
print_step "Switching to HTTPS mode..."
|
||
|
||
apply_console_protocol_mode https "$ssl_dir/betterdesk.crt" "$ssl_dir/betterdesk.key" "$api_tls" "$allow_self_signed"
|
||
|
||
sync_go_server_signal_relay_tls "$ssl_dir"
|
||
ensure_betterdesk_console_user >/dev/null
|
||
|
||
print_success "Switched to HTTPS mode"
|
||
echo ""
|
||
print_info " Panel: HTTPS :$(resolve_panel_https_port)"
|
||
print_info " Redirect: HTTP :$(resolve_panel_http_port) → HTTPS :$(resolve_panel_https_port)"
|
||
print_info " Signal: TLS :21116"
|
||
print_info " Relay: TLS :21117"
|
||
print_info " Go API: HTTP :${GO_API_PORT:-21114} (RustDesk client + REST)"
|
||
if [ "$api_tls" = "true" ]; then
|
||
print_info " Client API: HTTPS :${CLIENT_API_PORT:-21121}"
|
||
else
|
||
print_info " Client API: HTTP :${CLIENT_API_PORT:-21121}"
|
||
fi
|
||
maybe_offer_standard_https_port
|
||
;;
|
||
3)
|
||
do_configure_reverse_proxy || true
|
||
;;
|
||
0|4|*)
|
||
return
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
if confirm "Restart BetterDesk services now?"; then
|
||
repair_https_stuck_state yes
|
||
ensure_console_tls_material_readable 2>/dev/null || true
|
||
systemctl restart betterdesk-server betterdesk-console 2>/dev/null || true
|
||
verify_service_health "betterdesk-server" "21116" 15 >/dev/null 2>&1 || true
|
||
verify_service_health "betterdesk-console" "$(resolve_panel_health_port)" 15 >/dev/null 2>&1 || true
|
||
print_success "BetterDesk services restarted"
|
||
run_protocol_tests
|
||
else
|
||
print_info "Changes saved. Restart later with: systemctl restart betterdesk-server betterdesk-console"
|
||
fi
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Database Migration Functions
|
||
#===============================================================================
|
||
|
||
do_migrate_database() {
|
||
print_header
|
||
echo -e "${WHITE}${BOLD}══════════ DATABASE MIGRATION ══════════${NC}"
|
||
echo ""
|
||
|
||
# Locate migration binary
|
||
local migrate_bin=""
|
||
local arch=$(uname -m)
|
||
local search_paths=(
|
||
"$SCRIPT_DIR/betterdesk-server/tools/migrate/migrate-linux-amd64"
|
||
"$SCRIPT_DIR/tools/migrate/migrate-linux-amd64"
|
||
"$RUSTDESK_PATH/migrate"
|
||
"/usr/local/bin/betterdesk-migrate"
|
||
)
|
||
|
||
for p in "${search_paths[@]}"; do
|
||
if [ -f "$p" ] && [ -x "$p" ]; then
|
||
migrate_bin="$p"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ -z "$migrate_bin" ]; then
|
||
# Try to find non-executable and make it executable
|
||
for p in "${search_paths[@]}"; do
|
||
if [ -f "$p" ]; then
|
||
chmod +x "$p"
|
||
migrate_bin="$p"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
if [ -z "$migrate_bin" ]; then
|
||
print_error "Migration binary not found!"
|
||
print_info "Expected at: $SCRIPT_DIR/betterdesk-server/tools/migrate/migrate-linux-amd64"
|
||
print_info "Build it with: cd betterdesk-server && go build -o tools/migrate/migrate-linux-amd64 ./tools/migrate/"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
print_info "Migration binary: $migrate_bin"
|
||
echo ""
|
||
local _menu_items=(
|
||
$'Rust -> Go\tMigrate legacy Rust hbbs database to the Go server'
|
||
$'Node.js -> Go\tMigrate the Node.js web console DB to the Go server'
|
||
$'SQLite -> PostgreSQL\tMigrate BetterDesk Go SQLite to PostgreSQL'
|
||
$'PostgreSQL -> SQLite\tMigrate PostgreSQL back to SQLite'
|
||
$'Backup\tCreate a timestamped SQLite database backup'
|
||
$'Back\tReturn to the main menu'
|
||
)
|
||
local _menu_returns=( 1 2 3 4 5 0 )
|
||
menu_choose "Database Migration" "Migrate databases between BetterDesk components"
|
||
local mig_choice="$MENU_CHOICE"
|
||
|
||
case $mig_choice in
|
||
1)
|
||
# Rust → Go
|
||
echo ""
|
||
local default_src="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
read -p "Source Rust database [$default_src]: " src_db
|
||
src_db="${src_db:-$default_src}"
|
||
|
||
if [ ! -f "$src_db" ]; then
|
||
print_error "Source database not found: $src_db"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
read -p "Destination (SQLite path or postgres:// URI) [new file next to source]: " dst_db
|
||
|
||
print_step "Creating backup before migration..."
|
||
"$migrate_bin" -mode backup -src "$src_db" 2>&1 || true
|
||
|
||
print_step "Running Rust → Go migration..."
|
||
if [ -n "$dst_db" ]; then
|
||
"$migrate_bin" -mode rust2go -src "$src_db" -dst "$dst_db" 2>&1
|
||
else
|
||
"$migrate_bin" -mode rust2go -src "$src_db" 2>&1
|
||
fi
|
||
|
||
if [ $? -eq 0 ]; then
|
||
print_success "Rust → Go migration completed successfully!"
|
||
else
|
||
print_error "Migration failed. Check the output above for details."
|
||
fi
|
||
;;
|
||
2)
|
||
# Node.js → Go
|
||
echo ""
|
||
local default_src="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
local default_auth="$CONSOLE_PATH/data/auth.db"
|
||
|
||
read -p "Source Node.js peer database [$default_src]: " src_db
|
||
src_db="${src_db:-$default_src}"
|
||
|
||
if [ ! -f "$src_db" ]; then
|
||
print_error "Source peer database not found: $src_db"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
read -p "Node.js auth database [$default_auth]: " auth_db
|
||
auth_db="${auth_db:-$default_auth}"
|
||
|
||
read -p "Destination (SQLite path or postgres:// URI) [new file next to source]: " dst_db
|
||
|
||
print_step "Creating backup before migration..."
|
||
"$migrate_bin" -mode backup -src "$src_db" 2>&1 || true
|
||
if [ -f "$auth_db" ]; then
|
||
"$migrate_bin" -mode backup -src "$auth_db" 2>&1 || true
|
||
fi
|
||
|
||
print_step "Running Node.js → Go migration..."
|
||
# SECURITY (audit fix M-04, 2026-04-10): use a bash array + direct exec
|
||
# instead of cmd-string + eval to avoid shell injection if any input
|
||
# contains spaces / metacharacters.
|
||
local args=("-mode" "nodejs2go" "-src" "$src_db")
|
||
if [ -f "$auth_db" ]; then
|
||
args+=("-node-auth" "$auth_db")
|
||
fi
|
||
if [ -n "$dst_db" ]; then
|
||
args+=("-dst" "$dst_db")
|
||
fi
|
||
"$migrate_bin" "${args[@]}" 2>&1
|
||
|
||
if [ $? -eq 0 ]; then
|
||
print_success "Node.js → Go migration completed successfully!"
|
||
else
|
||
print_error "Migration failed. Check the output above for details."
|
||
fi
|
||
;;
|
||
3)
|
||
# SQLite → PostgreSQL
|
||
echo ""
|
||
local default_src="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
read -p "Source SQLite database [$default_src]: " src_db
|
||
src_db="${src_db:-$default_src}"
|
||
|
||
if [ ! -f "$src_db" ]; then
|
||
print_error "Source database not found: $src_db"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
read -p "PostgreSQL connection URI (postgres://user:pass@host:5432/dbname): " pg_uri
|
||
if [ -z "$pg_uri" ]; then
|
||
print_error "PostgreSQL URI is required"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
print_step "Creating backup before migration..."
|
||
"$migrate_bin" -mode backup -src "$src_db" 2>&1 || true
|
||
|
||
print_step "Running SQLite → PostgreSQL migration..."
|
||
"$migrate_bin" -mode sqlite2pg -src "$src_db" -dst "$pg_uri" 2>&1
|
||
|
||
if [ $? -eq 0 ]; then
|
||
print_success "SQLite → PostgreSQL migration completed successfully!"
|
||
print_info "Update your BetterDesk Go server config: DB_URL=$pg_uri"
|
||
else
|
||
print_error "Migration failed. Check the output above for details."
|
||
fi
|
||
;;
|
||
4)
|
||
# PostgreSQL → SQLite
|
||
echo ""
|
||
read -p "PostgreSQL connection URI (postgres://user:pass@host:5432/dbname): " pg_uri
|
||
if [ -z "$pg_uri" ]; then
|
||
print_error "PostgreSQL URI is required"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
local default_dst="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
read -p "Destination SQLite file [$default_dst]: " dst_db
|
||
dst_db="${dst_db:-$default_dst}"
|
||
|
||
if [ -f "$dst_db" ]; then
|
||
print_warning "Destination file exists: $dst_db"
|
||
if ! confirm "Overwrite (backup will be created first)?"; then
|
||
press_enter
|
||
return
|
||
fi
|
||
"$migrate_bin" -mode backup -src "$dst_db" 2>&1 || true
|
||
fi
|
||
|
||
print_step "Running PostgreSQL → SQLite migration..."
|
||
"$migrate_bin" -mode pg2sqlite -src "$pg_uri" -dst "$dst_db" 2>&1
|
||
|
||
if [ $? -eq 0 ]; then
|
||
print_success "PostgreSQL → SQLite migration completed successfully!"
|
||
else
|
||
print_error "Migration failed. Check the output above for details."
|
||
fi
|
||
;;
|
||
5)
|
||
# Backup
|
||
echo ""
|
||
local default_src="$RUSTDESK_PATH/db_v2.sqlite3"
|
||
read -p "SQLite database to backup [$default_src]: " src_db
|
||
src_db="${src_db:-$default_src}"
|
||
|
||
if [ ! -f "$src_db" ]; then
|
||
print_error "Database not found: $src_db"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
print_step "Creating backup..."
|
||
"$migrate_bin" -mode backup -src "$src_db" 2>&1
|
||
|
||
if [ $? -eq 0 ]; then
|
||
print_success "Backup created successfully!"
|
||
else
|
||
print_error "Backup failed."
|
||
fi
|
||
;;
|
||
0)
|
||
return
|
||
;;
|
||
*)
|
||
print_warning "Invalid option"
|
||
;;
|
||
esac
|
||
|
||
press_enter
|
||
}
|
||
|
||
#===============================================================================
|
||
# Main Menu
|
||
#===============================================================================
|
||
|
||
show_menu() {
|
||
print_header
|
||
print_status
|
||
|
||
echo -e "${WHITE}${BOLD}══════════ MAIN MENU ══════════${NC}"
|
||
echo ""
|
||
echo " 1. 🚀 FRESH INSTALLATION"
|
||
echo " 2. ⬆️ UPDATE"
|
||
echo " 3. 🔧 REPAIR INSTALLATION"
|
||
echo " 4. ✅ INSTALLATION VALIDATION"
|
||
echo " 5. 💾 Backup"
|
||
echo " 6. 🔐 Reset admin password"
|
||
echo " 7. 🔨 Build & deploy server"
|
||
echo " 8. 📊 DIAGNOSTICS"
|
||
echo " 9. 🗑️ UNINSTALL"
|
||
echo ""
|
||
echo " L. 📦 MINIMAL INSTALLATION (server only)"
|
||
echo " C. 🔒 Configure SSL certificates"
|
||
echo " T. 🔄 Toggle HTTP/HTTPS mode"
|
||
echo " M. 🔄 Database migration"
|
||
echo " B. 🧰 Build toolchain"
|
||
echo " S. ⚙️ Settings (paths)"
|
||
echo " 0. ❌ Exit"
|
||
echo ""
|
||
echo -e " ${DIM}Tip: this menu also supports arrow-key navigation (set BETTERDESK_CLASSIC_MENU=1 to force this list).${NC}"
|
||
echo ""
|
||
}
|
||
|
||
main() {
|
||
# Check root
|
||
if [ "$EUID" -ne 0 ]; then
|
||
echo -e "${RED}This script requires root privileges!${NC}"
|
||
echo "Run: sudo $0"
|
||
exit 1
|
||
fi
|
||
|
||
# Auto-detect paths on startup
|
||
echo -e "${CYAN}Detecting installation...${NC}"
|
||
auto_detect_paths
|
||
echo ""
|
||
sleep 1
|
||
|
||
# Auto mode - run installation directly
|
||
if [ "$AUTO_MODE" = true ]; then
|
||
print_info "Running in AUTO mode..."
|
||
if [ "$UNINSTALL_MODE" = true ]; then
|
||
do_uninstall
|
||
elif [ "$MINIMAL_MODE" = true ]; then
|
||
do_install_minimal
|
||
else
|
||
do_install
|
||
fi
|
||
exit $?
|
||
fi
|
||
|
||
# Action tokens map 1:1 to the classic case dispatch below, so both the
|
||
# arrow-key TUI and the numeric fallback share the exact same handlers.
|
||
local menu_labels=(
|
||
$'Fresh installation\tFull install from scratch'
|
||
$'Update\tUpdate an existing installation'
|
||
$'Repair installation\tFix common problems'
|
||
$'Validate installation\tCheck correctness'
|
||
$'Backup\tCreate a backup'
|
||
$'Reset admin password\tReset the console admin'
|
||
$'Build & deploy server\tCompile and deploy the Go server'
|
||
$'Diagnostics\tDetailed problem analysis'
|
||
$'Uninstall\tRemove BetterDesk'
|
||
$'Minimal installation\tServer only'
|
||
$'Configure SSL certificates\tLet'"'"'s Encrypt / custom / self-signed'
|
||
$'Toggle HTTP/HTTPS\tSwitch protocol + run tests'
|
||
$'Database migration\tMigrate between backends'
|
||
$'Build toolchain\tInstall compilers'
|
||
$'Settings (paths)\tConfigure install paths'
|
||
$'Exit\tQuit the manager'
|
||
)
|
||
local menu_actions=( 1 2 3 4 5 6 7 8 9 L C T M B S 0 )
|
||
|
||
while true; do
|
||
local choice=""
|
||
if tui_available; then
|
||
detect_installation 2>/dev/null
|
||
local status_line="Install: ${INSTALL_STATUS:-unknown}"
|
||
[ "$HBBS_RUNNING" = true ] && status_line="$status_line | server: running" || status_line="$status_line | server: stopped"
|
||
[ "$CONSOLE_RUNNING" = true ] && status_line="$status_line | console: running" || status_line="$status_line | console: stopped"
|
||
if tui_select "BetterDesk Console Manager v${VERSION}" "$status_line" "${menu_labels[@]}"; then
|
||
choice="${menu_actions[$TUI_RESULT]}"
|
||
else
|
||
choice="0"
|
||
fi
|
||
else
|
||
show_menu
|
||
read -p "Select option: " choice
|
||
fi
|
||
|
||
case $choice in
|
||
1) do_install ;;
|
||
2) do_update ;;
|
||
3) do_repair ;;
|
||
4) do_validate ;;
|
||
5) do_backup ;;
|
||
6) do_reset_password ;;
|
||
7) do_build ;;
|
||
8) do_diagnostics ;;
|
||
9) do_uninstall ;;
|
||
[Ll]) do_install_minimal ;;
|
||
[Cc]) do_configure_ssl ;;
|
||
[Tt]) do_toggle_protocol ;;
|
||
[Mm]) do_migrate_database ;;
|
||
[Bb]) do_install_build_toolchain ;;
|
||
[Ss]) configure_paths ;;
|
||
0)
|
||
echo ""
|
||
print_info "Goodbye!"
|
||
exit 0
|
||
;;
|
||
*)
|
||
print_warning "Invalid option"
|
||
sleep 1
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# Run
|
||
main "$@"
|