Files
BetterDesk/betterdesk.ps1
T
2026-08-23 13:27:37 +00:00

6097 lines
247 KiB
PowerShell

#Requires -RunAsAdministrator
<#
.SYNOPSIS
BetterDesk Console Manager v3.5.56 - All-in-One Interactive Tool for Windows
.DESCRIPTION
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
.PARAMETER Auto
Run installation in automatic mode (non-interactive)
.PARAMETER Uninstall
Stop services and remove the native installation; data is preserved by default
.PARAMETER Purge
With -Uninstall, also remove installation data and keys
.PARAMETER SkipVerify
Skip SHA256 verification of binaries
.PARAMETER NodeJs
Install Node.js web console (default)
.PARAMETER Protocol
Set protocol mode: 'http' or 'https'
.PARAMETER PostgreSQL
Use PostgreSQL instead of SQLite
.PARAMETER PgUri
PostgreSQL connection URI (implies -PostgreSQL)
.PARAMETER RelayMode
Relay IP selection mode: 'auto' (detect public IP, default), 'local'/'lan'
(use the server's LAN IP for LAN-only deployments), or 'public'/'wan'
(force public IP detection). Overridden by -RelayServers / RELAY_SERVERS.
.PARAMETER RelayServers
Force a fixed relay server address (IP or host[:port]). Always overrides
-RelayMode. Equivalent to the RELAY_SERVERS environment variable.
.PARAMETER RunAsRoot
Run the Windows services as LocalSystem (legacy). By default the services
run under low-privilege per-service virtual accounts (privilege separation).
.EXAMPLE
.\betterdesk.ps1
Interactive mode
.EXAMPLE
.\betterdesk.ps1 -Auto
Automatic installation with Node.js console and SQLite
.EXAMPLE
.\betterdesk.ps1 -Auto -PostgreSQL
Automatic installation with PostgreSQL
.EXAMPLE
.\betterdesk.ps1 -Auto -RelayMode local
Automatic LAN-only installation (relay uses the server's local IP)
.EXAMPLE
.\betterdesk.ps1 -Auto -RelayServers 203.0.113.10
Automatic installation with a fixed public relay address
.EXAMPLE
.\betterdesk.ps1 -SkipVerify
Skip binary verification
#>
param(
[switch]$Auto,
[switch]$Uninstall,
[switch]$Purge,
[switch]$SkipVerify,
[switch]$Minimal,
[switch]$NodeJs,
[switch]$PostgreSQL,
[string]$PgUri = "",
[ValidateSet('http', 'https', '')]
[string]$Protocol = "",
[ValidateSet('auto', 'local', 'lan', 'public', 'wan', '')]
[string]$RelayMode = "",
[string]$RelayServers = "",
[switch]$RunAsRoot,
[switch]$Flask # Deprecated, kept for backward compatibility
)
#===============================================================================
# Configuration
#===============================================================================
$script:VERSION = "3.5.56"
$script:ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Auto mode flags
$script:AUTO_MODE = $Auto
$script:UNINSTALL_MODE = $Uninstall
$script:PURGE_MODE = $Purge
$script:SKIP_VERIFY = $SkipVerify
$script:MINIMAL_MODE = $Minimal
# Privilege separation (default). The installer needs Administrator, but the
# services run under low-privilege per-service virtual accounts (NT SERVICE\...)
# instead of LocalSystem. Use -RunAsRoot or BETTERDESK_RUN_AS_ROOT=1 to keep the
# legacy behavior of running services as LocalSystem.
$script:RUN_AS_ROOT = $RunAsRoot -or ($env:BETTERDESK_RUN_AS_ROOT -eq "1") -or ($env:BETTERDESK_RUN_AS_ROOT -eq "true")
# Console type preference
$script:PREFERRED_CONSOLE_TYPE = "nodejs" # Always Node.js (Flask removed in v2.3.0)
if ($Flask) {
Write-Host "WARNING: Flask console is deprecated. Node.js will be installed instead." -ForegroundColor Yellow
$script:PREFERRED_CONSOLE_TYPE = "nodejs"
}
# Database configuration
$script:USE_POSTGRESQL = $PostgreSQL -or ($env:USE_POSTGRESQL -eq "true")
$script:POSTGRESQL_URI = if ($PgUri) { $PgUri } elseif ($env:POSTGRESQL_URI) { $env:POSTGRESQL_URI } else { "" }
$script:POSTGRESQL_USER = if ($env:POSTGRESQL_USER) { $env:POSTGRESQL_USER } else { "betterdesk" }
$script:POSTGRESQL_PASS = if ($env:POSTGRESQL_PASS) { $env:POSTGRESQL_PASS } else { "" }
$script:POSTGRESQL_DB = if ($env:POSTGRESQL_DB) { $env:POSTGRESQL_DB } else { "betterdesk" }
$script:POSTGRESQL_HOST = if ($env:POSTGRESQL_HOST) { $env:POSTGRESQL_HOST } else { "localhost" }
$script:POSTGRESQL_PORT = if ($env:POSTGRESQL_PORT) { $env:POSTGRESQL_PORT } else { "5432" }
# Relay server configuration
# 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 -RelayServers) always overrides this with a fixed value.
$script:RELAY_MODE = if ($RelayMode) { $RelayMode } elseif ($env:RELAY_MODE) { $env:RELAY_MODE } else { "auto" }
if ($script:RELAY_MODE -eq "lan") { $script:RELAY_MODE = "local" }
if ($script:RELAY_MODE -eq "wan") { $script:RELAY_MODE = "public" }
$script:RELAY_SERVERS = if ($RelayServers) { $RelayServers } elseif ($env:RELAY_SERVERS) { $env:RELAY_SERVERS } else { "" }
# Go server configuration
$script:GO_SERVER_SOURCE = Join-Path $script:ScriptDir "betterdesk-server"
# Must match the toolchain pinned in betterdesk-server/go.mod.
$script:GO_MIN_VERSION = "1.26.6"
$script:GO_DOWNLOAD_VERSION = "1.26.6"
# Legacy Rust checksums (deprecated, kept for migration purposes)
$script:HBBS_WINDOWS_X86_64_SHA256 = "B790FA44CAC7482A057ED322412F6D178FB33F3B05327BFA753416E9879BD62F"
$script:HBBR_WINDOWS_X86_64_SHA256 = "368C71E8D3AEF4C5C65177FBBBB99EA045661697A89CB7C2A703759C575E8E9F"
# Default paths
$script:RUSTDESK_PATH = if ($env:RUSTDESK_PATH) { $env:RUSTDESK_PATH } else { "C:\BetterDesk" }
$script:CONSOLE_PATH = if ($env:CONSOLE_PATH) { $env:CONSOLE_PATH } else { "C:\BetterDeskConsole" }
$script:BACKUP_DIR = if ($env:BACKUP_DIR) { $env:BACKUP_DIR } else { "C:\BetterDesk-Backups" }
$script:DB_PATH = "$script:RUSTDESK_PATH\db_v2.sqlite3"
# API configuration
$script:GO_API_PORT = if ($env:GO_API_PORT) { $env:GO_API_PORT } elseif ($env:API_PORT) { $env:API_PORT } else { "21114" }
$script:CLIENT_API_PORT = if ($env:CLIENT_API_PORT) { $env:CLIENT_API_PORT } else { "21121" }
$script:API_PORT = $script:GO_API_PORT
$script:STORE_ADMIN_CREDENTIALS = ($env:STORE_ADMIN_CREDENTIALS -eq "true")
# Common installation paths to search
$script:COMMON_RUSTDESK_PATHS = @(
"C:\BetterDesk",
"C:\RustDesk",
"C:\Program Files\BetterDesk",
"C:\Program Files\RustDesk",
"$env:LOCALAPPDATA\BetterDesk"
)
$script:COMMON_CONSOLE_PATHS = @(
"C:\BetterDeskConsole",
"C:\Program Files\BetterDeskConsole",
"$env:LOCALAPPDATA\BetterDeskConsole"
)
# Service names
$script:SERVER_SERVICE = "BetterDeskServer" # Go server (replaces HBBS + HBBR)
$script:HBBS_SERVICE = "BetterDeskSignal" # Legacy Rust signal
$script:HBBR_SERVICE = "BetterDeskRelay" # Legacy Rust relay
$script:CONSOLE_SERVICE = "BetterDeskConsole"
# Status variables
$script:INSTALL_STATUS = "none"
$script:SERVER_RUNNING = $false # Go server
$script:HBBS_RUNNING = $false # Legacy Rust
$script:HBBR_RUNNING = $false # Legacy Rust
$script:CONSOLE_RUNNING = $false
$script:BINARIES_OK = $false
$script:DATABASE_OK = $false
$script:CONSOLE_TYPE = "none" # none, nodejs
$script:SERVER_TYPE = "none" # none, go, rust
# FRESH_INSTALL gates new-install defaults (e.g. managed enrollment mode). It
# stays $false for UPDATE/REPAIR so existing installs keep their current policy.
$script:FRESH_INSTALL = $false
# Logging
$script:LOG_FILE = "$env:TEMP\betterdesk_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
#===============================================================================
# Helper Functions
#===============================================================================
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"[$timestamp] $Message" | Out-File -FilePath $script:LOG_FILE -Append -Encoding UTF8
}
function Print-Header {
Clear-Host
Write-Host @"
Console Manager v$($script:VERSION)
"@ -ForegroundColor Cyan
Write-Host ""
Write-Host " ⚠ WINDOWS SUPPORT: EXPERIMENTAL (Tier 3)" -ForegroundColor Yellow
Write-Host " Primary platform: Linux. Report issues at GitHub." -ForegroundColor DarkYellow
Write-Host ""
}
function Print-Success {
param([string]$Message)
Write-Host "[OK] " -ForegroundColor Green -NoNewline
Write-Host $Message
Write-Log "SUCCESS: $Message"
}
function Print-Error {
param([string]$Message)
Write-Host "[X] " -ForegroundColor Red -NoNewline
Write-Host $Message
Write-Log "ERROR: $Message"
}
function Print-Warning {
param([string]$Message)
Write-Host "[!] " -ForegroundColor Yellow -NoNewline
Write-Host $Message
Write-Log "WARNING: $Message"
}
function Print-Info {
param([string]$Message)
Write-Host "[i] " -ForegroundColor Blue -NoNewline
Write-Host $Message
Write-Log "INFO: $Message"
}
function Print-Step {
param([string]$Message)
Write-Host "[>] " -ForegroundColor Magenta -NoNewline
Write-Host $Message
Write-Log "STEP: $Message"
}
function Press-Enter {
Write-Host ""
Write-Host "Press Enter to continue..." -ForegroundColor Cyan
if (-not $script:AUTO_MODE) {
$null = Read-Host
}
}
function Confirm-Action {
param([string]$Prompt = "Continue?")
if ($script:AUTO_MODE) { return $true }
$response = Read-Host "$Prompt [y/N]"
return $response -match "^[YyTt]"
}
#===============================================================================
# Interactive TUI (arrow-key navigable menu) — no external dependencies
#===============================================================================
$script:TUI_RESULT = -1
$script:MENU_CHOICE = ''
function Test-TuiAvailable {
if ($env:BETTERDESK_CLASSIC_MENU -eq '1') { return $false }
if ($script:AUTO_MODE) { return $false }
try {
if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) { return $false }
} catch { return $false }
return $true
}
# Invoke-TuiSelect -Title T -Subtitle S -Items @("Label`tDesc", ...)
# Navigation: Up/Down or k/j to move, Enter/Right to choose, q/Esc/0 to cancel.
# Returns $true and sets $script:TUI_RESULT on selection, $false on cancel.
function Invoke-TuiSelect {
param(
[string]$Title,
[string]$Subtitle,
[string[]]$Items
)
$script:TUI_RESULT = -1
$count = $Items.Count
if (-not (Test-TuiAvailable) -or $count -eq 0) { return $false }
$sel = 0
try { [Console]::CursorVisible = $false } catch {}
Clear-Host
try {
while ($true) {
try { [Console]::SetCursorPosition(0, 0) } catch {}
Write-Host "+--------------------------------------------------------------+" -ForegroundColor Cyan
Write-Host ("| {0,-60} |" -f $Title) -ForegroundColor White
if ($Subtitle) { Write-Host ("| {0,-60} |" -f $Subtitle) -ForegroundColor DarkGray }
Write-Host "+--------------------------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
for ($i = 0; $i -lt $count; $i++) {
$parts = $Items[$i] -split "`t", 2
$label = $parts[0]
$desc = if ($parts.Count -gt 1) { $parts[1] } else { '' }
if ($i -eq $sel) {
$line = (" > {0,-30}{1}" -f $label, $desc)
Write-Host ($line.PadRight(78)) -ForegroundColor Green
} else {
$line = (" {0,-30}{1}" -f $label, $desc)
Write-Host ($line.PadRight(78)) -ForegroundColor White
}
}
Write-Host ""
Write-Host (" Up/Down navigate Enter select q/Esc back".PadRight(78)) -ForegroundColor DarkGray
$key = [Console]::ReadKey($true)
switch ($key.Key) {
'UpArrow' { $sel = (($sel - 1 + $count) % $count) }
'DownArrow' { $sel = (($sel + 1) % $count) }
'Enter' { $script:TUI_RESULT = $sel; return $true }
'RightArrow' { $script:TUI_RESULT = $sel; return $true }
'Escape' { return $false }
default {
$ch = $key.KeyChar
if ($ch -eq 'k') { $sel = (($sel - 1 + $count) % $count) }
elseif ($ch -eq 'j') { $sel = (($sel + 1) % $count) }
elseif ($ch -eq 'q' -or $ch -eq 'Q' -or $ch -eq '0') { return $false }
elseif ($ch -ge '1' -and $ch -le '9') {
$idx = [int]::Parse($ch) - 1
if ($idx -lt $count) { $script:TUI_RESULT = $idx; return $true }
}
}
}
}
} finally {
try { [Console]::CursorVisible = $true } catch {}
}
}
function Show-PanelHeader {
param([string]$Title, [string]$Subtitle)
Clear-Host
Write-Host "+--------------------------------------------------------------+" -ForegroundColor Cyan
Write-Host ("| {0,-60} |" -f $Title) -ForegroundColor White
if ($Subtitle) { Write-Host ("| {0,-60} |" -f $Subtitle) -ForegroundColor DarkGray }
Write-Host "+--------------------------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
}
# Invoke-MenuChoose -Title T -Subtitle S -Items @(...) -Returns @(...)
# Uses the arrow-key TUI when available, a styled numeric prompt otherwise.
# The chosen token is stored in $script:MENU_CHOICE; on cancel the last entry
# is returned so existing switch blocks can treat it as "back".
function Invoke-MenuChoose {
param(
[string]$Title,
[string]$Subtitle,
[string[]]$Items,
[string[]]$Returns
)
$script:MENU_CHOICE = ''
$lastIdx = $Returns.Count - 1
if ($lastIdx -lt 0) { $lastIdx = 0 }
if (Test-TuiAvailable) {
if (Invoke-TuiSelect -Title $Title -Subtitle $Subtitle -Items $Items) {
$script:MENU_CHOICE = $Returns[$script:TUI_RESULT]
} else {
$script:MENU_CHOICE = $Returns[$lastIdx]
}
return
}
Show-PanelHeader $Title $Subtitle
for ($i = 0; $i -lt $Items.Count; $i++) {
$parts = $Items[$i] -split "`t", 2
$label = $parts[0]
$desc = if ($parts.Count -gt 1) { $parts[1] } else { '' }
Write-Host (" {0,2}) " -f $Returns[$i]) -ForegroundColor Green -NoNewline
Write-Host ("{0,-28}" -f $label) -NoNewline
Write-Host " $desc" -ForegroundColor DarkGray
}
Write-Host ""
$script:MENU_CHOICE = Read-Host " Select option"
}
function Get-PublicIP {
# Prefer IPv4 endpoints first: many RustDesk clients cannot use IPv6-only relay.
$endpoints = @(
"https://ipv4.icanhazip.com",
"https://ifconfig.me/ip",
"https://icanhazip.com"
)
foreach ($url in $endpoints) {
try {
$ip = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10).Content.Trim()
if ($ip) { return $ip }
} catch {}
}
return "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.
function Get-LocalIP {
try {
# Primary: source address used to reach an external destination
$route = Get-NetIPConfiguration -ErrorAction SilentlyContinue |
Where-Object { $_.IPv4DefaultGateway -and $_.NetAdapter.Status -eq 'Up' } |
Select-Object -First 1
if ($route -and $route.IPv4Address) {
return $route.IPv4Address.IPAddress
}
} catch {}
try {
# Fallback: first non-loopback, non-APIPA IPv4 address
$ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' -and $_.PrefixOrigin -ne 'WellKnown' } |
Select-Object -First 1
if ($ip) { return $ip.IPAddress }
} catch {}
return "127.0.0.1"
}
function Resolve-ConnectionModeEnv {
$mode = if ($env:CONNECTION_MODE) { $env:CONNECTION_MODE } else { "p2p_first" }
if (-not $script:AUTO_MODE -and -not $env:CONNECTION_MODE_SET) {
Write-Host ""
Print-Info "Connection strategy: P2P hole punch vs relay-only routing."
Write-Host " 1) P2P first (recommended) — try direct, fall back to relay" -ForegroundColor Cyan
Write-Host " 2) Relay only — all sessions via relay server" -ForegroundColor Cyan
$choice = Read-Host " Select connection mode [1]"
if ($choice -eq "2") { $mode = "relay_only" } else { $mode = "p2p_first" }
Write-Host ""
}
$fallbackMs = if ($env:P2P_FALLBACK_MS) { $env:P2P_FALLBACK_MS } else { "2000" }
$sameNat = if ($env:SAME_NAT_RELAY) { $env:SAME_NAT_RELAY } else { "Y" }
if ($mode -eq "relay_only") {
$p2pFirst = "N"
$alwaysRelay = "Y"
} else {
$p2pFirst = "Y"
$alwaysRelay = "N"
}
Print-Info "Connection mode: $mode (P2P_FIRST=$p2pFirst, ALWAYS_USE_RELAY=$alwaysRelay)"
return @(
"P2P_FIRST=$p2pFirst",
"ALWAYS_USE_RELAY=$alwaysRelay",
"P2P_FALLBACK_MS=$fallbackMs",
"SAME_NAT_RELAY=$sameNat"
)
}
# Resolve the relay server address according to RELAY_MODE / RELAY_SERVERS.
# Returns the resolved address; warnings are written to the host (not the value).
function Resolve-RelayIp {
# Explicit override always wins
if ($script:RELAY_SERVERS) {
Print-Info "Using fixed relay address (RelayServers): $($script:RELAY_SERVERS)"
return $script:RELAY_SERVERS
}
$ip = ""
switch ($script:RELAY_MODE) {
"local" {
$ip = Get-LocalIP
Print-Info "Relay mode 'local': using LAN IP $ip (LAN-only deployment)"
}
"public" {
$ip = Get-PublicIP
Print-Info "Relay mode 'public': using public IP $ip"
}
default {
$ip = Get-PublicIP
if ($ip -eq "127.0.0.1" -or $ip -match '^10\.' -or $ip -match '^192\.168\.' -or $ip -match '^172\.(1[6-9]|2[0-9]|3[0-1])\.') {
Print-Warning "Auto-detected private/loopback IP: $ip"
Print-Warning "Remote (internet) clients will NOT connect via relay with this address."
Print-Warning "For LAN-only use this is fine. For internet access run with: -RelayServers YOUR.PUBLIC.IP"
Print-Warning "To use the LAN IP explicitly run with: -RelayMode local"
}
}
}
return $ip
}
function Generate-RandomPassword {
param([int]$Length = 16)
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
$password = -join ((1..$Length) | ForEach-Object { $chars[(Get-Random -Maximum $chars.Length)] })
return $password
}
#===============================================================================
# Detection Functions
#===============================================================================
function Detect-Installation {
$script:INSTALL_STATUS = "none"
$script:SERVER_RUNNING = $false
$script:HBBS_RUNNING = $false
$script:HBBR_RUNNING = $false
$script:CONSOLE_RUNNING = $false
$script:BINARIES_OK = $false
$script:DATABASE_OK = $false
$script:CONSOLE_TYPE = "none"
$script:SERVER_TYPE = "none"
# Check paths and binary type
if (Test-Path $script:RUSTDESK_PATH) {
# Check for Go server first
if (Test-Path "$script:RUSTDESK_PATH\betterdesk-server.exe") {
$script:BINARIES_OK = $true
$script:SERVER_TYPE = "go"
$script:INSTALL_STATUS = "partial"
}
# Fallback: Check for legacy Rust binaries
elseif ((Test-Path "$script:RUSTDESK_PATH\hbbs.exe") -or (Test-Path "$script:RUSTDESK_PATH\hbbs-v8-api.exe")) {
$script:BINARIES_OK = $true
$script:SERVER_TYPE = "rust"
$script:INSTALL_STATUS = "partial"
Print-Warning "Legacy Rust binaries detected. Consider upgrading to Go server."
}
}
# Check database (SQLite file or PostgreSQL)
$detectedDbType = "sqlite"
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$dbTypeLine = Select-String -Path $envFile -Pattern '^DB_TYPE=' -SimpleMatch | Select-Object -First 1
if ($dbTypeLine) {
$detectedDbType = ($dbTypeLine.Line -split '=', 2)[1].Trim()
}
}
if ($detectedDbType -eq "postgres") {
# PostgreSQL: we trust the config -- full validation is done by Do-Validate
$script:DATABASE_OK = $true
} elseif (Test-Path "$script:RUSTDESK_PATH\db_v2.sqlite3") {
$script:DATABASE_OK = $true
}
# Detect console type
if (Test-Path $script:CONSOLE_PATH) {
if ((Test-Path "$script:CONSOLE_PATH\server.js") -or (Test-Path "$script:CONSOLE_PATH\package.json")) {
$script:CONSOLE_TYPE = "nodejs"
} elseif (Test-Path "$script:CONSOLE_PATH\app.py") {
$script:CONSOLE_TYPE = "nodejs" # Legacy Flask, will be migrated
Print-Warning "Legacy Flask console detected. Will be migrated to Node.js on update."
}
if ($script:CONSOLE_TYPE -ne "none" -and $script:BINARIES_OK) {
$script:INSTALL_STATUS = "complete"
}
}
# Check services - Go server first
$serverService = Get-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
if ($serverService -and $serverService.Status -eq 'Running') {
$script:SERVER_RUNNING = $true
$script:HBBS_RUNNING = $true # Go handles both
$script:HBBR_RUNNING = $true
} else {
# Check legacy Rust services
$hbbsService = Get-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue
if ($hbbsService -and $hbbsService.Status -eq 'Running') {
$script:HBBS_RUNNING = $true
}
$hbbrService = Get-Service -Name $script:HBBR_SERVICE -ErrorAction SilentlyContinue
if ($hbbrService -and $hbbrService.Status -eq 'Running') {
$script:HBBR_RUNNING = $true
}
}
$consoleService = Get-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
if ($consoleService -and $consoleService.Status -eq 'Running') {
$script:CONSOLE_RUNNING = $true
}
}
# Preserve database configuration from existing .env file
# This MUST be called before Install-NodeJsConsole during UPDATE/REPAIR
# to prevent switching from PostgreSQL to SQLite
function Preserve-DatabaseConfig {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
# Read existing DB_TYPE
$dbTypeLine = Select-String -Path $envFile -Pattern '^DB_TYPE=' -SimpleMatch | Select-Object -First 1
$existingDbType = if ($dbTypeLine) { ($dbTypeLine.Line -split '=', 2)[1].Trim() } else { "" }
# Read existing DATABASE_URL
$dbUrlLine = Select-String -Path $envFile -Pattern '^DATABASE_URL=' -SimpleMatch | Select-Object -First 1
$existingDbUrl = if ($dbUrlLine) { ($dbUrlLine.Line -split '=', 2)[1].Trim() } else { "" }
if ($existingDbType -eq "postgres" -and $existingDbUrl) {
$script:USE_POSTGRESQL = $true
$script:POSTGRESQL_URI = $existingDbUrl
Print-Info "Preserving PostgreSQL configuration from existing .env"
} elseif ($existingDbType -eq "sqlite") {
$script:USE_POSTGRESQL = $false
$script:POSTGRESQL_URI = ""
Print-Info "Preserving SQLite configuration from existing .env"
}
}
}
# Write or merge console .env from web-nodejs/.env.example (issue #158).
function Merge-ConsoleEnv {
param(
[bool]$FreshInstall = $false
)
$mergeScript = Join-Path $script:CONSOLE_PATH "scripts\merge-env.js"
if (-not (Test-Path $mergeScript)) {
$mergeScript = Join-Path $script:ScriptDir "web-nodejs\scripts\merge-env.js"
}
if (-not (Test-Path $mergeScript)) {
Print-Error "merge-env.js not found — cannot configure .env"
return $false
}
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
$dbType = "sqlite"
$databaseUrl = ""
if ($script:USE_POSTGRESQL -and $script:POSTGRESQL_URI) {
$dbType = "postgres"
$databaseUrl = $script:POSTGRESQL_URI
}
$adminPassword = $env:ADMIN_PASSWORD
$sessionSecret = ""
if ($FreshInstall) {
if (-not $adminPassword) {
$adminPassword = Generate-RandomPassword
}
$sessionSecret = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 64 | ForEach-Object { [char]$_ })
} else {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$ssLine = Select-String -Path $envFile -Pattern '^SESSION_SECRET=' -SimpleMatch | Select-Object -First 1
$sessionSecret = if ($ssLine) { ($ssLine.Line -split '=', 2)[1].Trim() } else { "" }
if (-not $adminPassword) {
$apLine = Select-String -Path $envFile -Pattern '^DEFAULT_ADMIN_PASSWORD=' -SimpleMatch | Select-Object -First 1
$adminPassword = if ($apLine) { ($apLine.Line -split '=', 2)[1].Trim() } else { "" }
}
}
if (-not $sessionSecret) {
$sessionSecret = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 64 | ForEach-Object { [char]$_ })
}
}
$goPort = if ($script:GO_API_PORT) { $script:GO_API_PORT } else { 21114 }
$clientPort = if ($script:CLIENT_API_PORT) { $script:CLIENT_API_PORT } else { 21121 }
$dataDir = Join-Path $script:CONSOLE_PATH "data"
$substScript = Join-Path $script:CONSOLE_PATH "scripts\write-installer-env-subst.js"
if (-not (Test-Path $substScript)) {
$substScript = Join-Path $script:ScriptDir "web-nodejs\scripts\write-installer-env-subst.js"
}
$substFile = Join-Path $env:TEMP "betterdesk-env-subst-$PID.json"
$env:BD_SUBST_RUSTDESK_DIR = $script:RUSTDESK_PATH
$env:BD_SUBST_PUB_KEY_PATH = Join-Path $script:RUSTDESK_PATH "id_ed25519.pub"
$env:BD_SUBST_API_KEY_PATH = Join-Path $script:RUSTDESK_PATH ".api_key"
$env:BD_SUBST_DB_TYPE = $dbType
$env:BD_SUBST_DB_PATH = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$env:BD_SUBST_DATABASE_URL = $databaseUrl
$env:BD_SUBST_DATA_DIR = $dataDir
$env:BD_SUBST_GO_API_PORT = [string]$goPort
$env:BD_SUBST_HBBS_API_URL = "http://localhost:${goPort}/api"
$env:BD_SUBST_BETTERDESK_API_URL = "http://localhost:${goPort}/api"
$env:BD_SUBST_API_PORT = [string]$clientPort
$env:BD_SUBST_DEFAULT_ADMIN_PASSWORD = [string]$adminPassword
$env:BD_SUBST_SESSION_SECRET = [string]$sessionSecret
$env:BD_SUBST_SSL_CERT_PATH = Join-Path $sslDir "betterdesk.crt"
$env:BD_SUBST_SSL_KEY_PATH = Join-Path $sslDir "betterdesk.key"
if (-not (Test-Path $substScript)) {
Print-Error "write-installer-env-subst.js not found"
return $false
}
& node $substScript $substFile 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $substFile)) {
Print-Error "Failed to build .env substitution file"
return $false
}
$nodeArgs = @(
$mergeScript,
"--target", (Join-Path $script:CONSOLE_PATH ".env"),
"--subst-file", $substFile
)
if ($FreshInstall) { $nodeArgs += "--fresh" }
$output = & node @nodeArgs 2>&1
Remove-Item -Path $substFile -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0) {
Print-Error "Failed to write .env via merge-env.js: $output"
return $false
}
if ($FreshInstall) {
Print-Info "Created .env configuration file (fresh install)"
} else {
Print-Info "Merged new .env keys (existing settings preserved)"
}
return $true
}
function Auto-DetectPaths {
$found = $false
# Check configured path first - Go server or legacy Rust
if ($script:RUSTDESK_PATH -and (Test-Path $script:RUSTDESK_PATH)) {
if ((Test-Path "$script:RUSTDESK_PATH\betterdesk-server.exe") -or
(Test-Path "$script:RUSTDESK_PATH\hbbs.exe") -or
(Test-Path "$script:RUSTDESK_PATH\hbbs-v8-api.exe")) {
Print-Info "Using configured RustDesk path: $script:RUSTDESK_PATH"
$found = $true
}
}
# Auto-detect if not found
if (-not $found) {
foreach ($path in $script:COMMON_RUSTDESK_PATHS) {
if ((Test-Path $path) -and
((Test-Path "$path\betterdesk-server.exe") -or
(Test-Path "$path\hbbs.exe") -or
(Test-Path "$path\hbbs-v8-api.exe"))) {
$script:RUSTDESK_PATH = $path
Print-Success "Detected RustDesk installation: $script:RUSTDESK_PATH"
$found = $true
break
}
}
}
# Default path for new installations
if (-not $found) {
$script:RUSTDESK_PATH = "C:\BetterDesk"
Print-Info "No installation detected. Default path: $script:RUSTDESK_PATH"
}
# Auto-detect Console path and type
$consoleFound = $false
$script:CONSOLE_TYPE = "none"
foreach ($path in $script:COMMON_CONSOLE_PATHS) {
# Check for Node.js console first (server.js or package.json)
if ((Test-Path $path) -and ((Test-Path "$path\server.js") -or (Test-Path "$path\package.json"))) {
$script:CONSOLE_PATH = $path
$script:CONSOLE_TYPE = "nodejs"
Print-Success "Detected Node.js Console: $script:CONSOLE_PATH"
$consoleFound = $true
break
}
# Check for legacy Flask/Python console (app.py) - migrate to Node.js
if ((Test-Path $path) -and (Test-Path "$path\app.py") -and -not (Test-Path "$path\server.js")) {
$script:CONSOLE_PATH = $path
$script:CONSOLE_TYPE = "nodejs" # Will be migrated
Print-Warning "Legacy Flask console detected at $path. Will be migrated to Node.js."
$consoleFound = $true
break
}
}
if (-not $consoleFound) {
$script:CONSOLE_PATH = "C:\BetterDeskConsole"
}
# Update DB_PATH
$script:DB_PATH = "$script:RUSTDESK_PATH\db_v2.sqlite3"
}
function Print-Status {
Detect-Installation
Write-Host ""
Write-Host "=== System Status ===" -ForegroundColor White
Write-Host ""
Write-Host " System: " -NoNewline; Write-Host "Windows $([System.Environment]::OSVersion.Version)" -ForegroundColor Cyan
Write-Host " Architecture: " -NoNewline; Write-Host $env:PROCESSOR_ARCHITECTURE -ForegroundColor Cyan
Write-Host ""
Write-Host "=== Configured Paths ===" -ForegroundColor White
Write-Host ""
Write-Host " RustDesk: " -NoNewline; Write-Host $script:RUSTDESK_PATH -ForegroundColor Cyan
Write-Host " Console: " -NoNewline; Write-Host $script:CONSOLE_PATH -ForegroundColor Cyan
Write-Host " Database: " -NoNewline; Write-Host $script:DB_PATH -ForegroundColor Cyan
Write-Host ""
Write-Host "=== Installation Status ===" -ForegroundColor White
Write-Host ""
switch ($script:INSTALL_STATUS) {
"complete" { Write-Host " Status: " -NoNewline; Write-Host "[OK] Installed" -ForegroundColor Green }
"partial" { Write-Host " Status: " -NoNewline; Write-Host "[!] Partial installation" -ForegroundColor Yellow }
"none" { Write-Host " Status: " -NoNewline; Write-Host "[X] Not installed" -ForegroundColor Red }
}
if ($script:BINARIES_OK) {
$serverLabel = if ($script:SERVER_TYPE -eq "go") { " (Go: signal + relay + API)" } else { " (Legacy Rust)" }
Write-Host " Server: " -NoNewline; Write-Host "[OK]$serverLabel" -ForegroundColor Green
} else {
Write-Host " Server: " -NoNewline; Write-Host "[X] Not found" -ForegroundColor Red
}
if ($script:DATABASE_OK) {
Write-Host " Database: " -NoNewline; Write-Host "[OK]" -ForegroundColor Green
} else {
Write-Host " Database: " -NoNewline; Write-Host "[X] Not found" -ForegroundColor Red
}
if (Test-Path $script:CONSOLE_PATH) {
$consoleTypeLabel = switch ($script:CONSOLE_TYPE) {
"nodejs" { " (Node.js)" }
default { "" }
}
Write-Host " Web Console: " -NoNewline; Write-Host "[OK]$consoleTypeLabel" -ForegroundColor Green
} else {
Write-Host " Web Console: " -NoNewline; Write-Host "[X] Not found" -ForegroundColor Red
}
Write-Host ""
Write-Host "=== Services Status ===" -ForegroundColor White
Write-Host ""
# Check if using Go server or legacy Rust
if ($script:SERVER_RUNNING -or $script:SERVER_TYPE -eq "go") {
if ($script:SERVER_RUNNING) {
Write-Host " BetterDesk Server (Go): " -NoNewline; Write-Host "* Active (Signal + Relay + API)" -ForegroundColor Green
} else {
# Check service state for better diagnostics
$svc = Get-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq 'Stopped') {
Write-Host " BetterDesk Server (Go): " -NoNewline; Write-Host "o Stopped" -ForegroundColor Red
Write-Host " Hint: Check logs at $script:RUSTDESK_PATH\logs\server_error.log" -ForegroundColor Yellow
} else {
Write-Host " BetterDesk Server (Go): " -NoNewline; Write-Host "o Inactive" -ForegroundColor Red
}
}
} else {
# Legacy Rust services
if ($script:HBBS_RUNNING) {
Write-Host " HBBS (Signal): " -NoNewline; Write-Host "* Active " -ForegroundColor Green -NoNewline
Write-Host "(Legacy Rust)" -ForegroundColor Yellow
} else {
Write-Host " HBBS (Signal): " -NoNewline; Write-Host "o Inactive" -ForegroundColor Red
}
if ($script:HBBR_RUNNING) {
Write-Host " HBBR (Relay): " -NoNewline; Write-Host "* Active " -ForegroundColor Green -NoNewline
Write-Host "(Legacy Rust)" -ForegroundColor Yellow
} else {
Write-Host " HBBR (Relay): " -NoNewline; Write-Host "o Inactive" -ForegroundColor Red
}
}
if ($script:CONSOLE_RUNNING) {
Write-Host " Web Console: " -NoNewline; Write-Host "* Active" -ForegroundColor Green
} else {
$consoleSvc = Get-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
if ($consoleSvc -and $consoleSvc.Status -eq 'Stopped') {
Write-Host " Web Console: " -NoNewline; Write-Host "o Stopped" -ForegroundColor Red
Write-Host " Hint: Check logs at $script:CONSOLE_PATH\logs\console_error.log" -ForegroundColor Yellow
} else {
Write-Host " Web Console: " -NoNewline; Write-Host "o Inactive" -ForegroundColor Red
}
}
Write-Host ""
}
#===============================================================================
# Go Installation and Compilation Functions
#===============================================================================
function Get-GoVersionPart {
param(
[string]$Version,
[int]$Index = 0
)
$normalized = ($Version -replace '[^0-9.]', '')
$parts = $normalized -split '\.'
if ($Index -lt $parts.Count -and $parts[$Index] -match '^\d+$') {
return [int]$parts[$Index]
}
return 0
}
function Test-GoInstalled {
$goCmd = Get-Command go -ErrorAction SilentlyContinue
if (-not $goCmd) {
return $false
}
$goVersionOutput = & go version 2>&1
$goMatch = [regex]::Match($goVersionOutput, 'go(\d+)\.(\d+)(?:\.(\d+))?')
if (-not $goMatch.Success) {
return $false
}
$goVersion = $goMatch.Groups[0].Value.Replace('go', '')
$currentMajor = [int]$goMatch.Groups[1].Value
$currentMinor = [int]$goMatch.Groups[2].Value
$currentPatch = if ($goMatch.Groups[3].Success) { [int]$goMatch.Groups[3].Value } else { 0 }
# Security hardening: reject vulnerable Go 1.26.0 stdlib.
if ($currentMajor -eq 1 -and $currentMinor -eq 26 -and $currentPatch -eq 0) {
Print-Warning "Detected vulnerable Go version $goVersion (known stdlib CVEs)."
return $false
}
$minMajor = Get-GoVersionPart -Version $script:GO_MIN_VERSION -Index 0
$minMinor = Get-GoVersionPart -Version $script:GO_MIN_VERSION -Index 1
$minPatch = Get-GoVersionPart -Version $script:GO_MIN_VERSION -Index 2
if ($currentMajor -gt $minMajor -or
($currentMajor -eq $minMajor -and $currentMinor -gt $minMinor) -or
($currentMajor -eq $minMajor -and $currentMinor -eq $minMinor -and $currentPatch -ge $minPatch)) {
return $true
}
Print-Warning "Go version $goVersion is older than required $script:GO_MIN_VERSION"
return $false
}
function Install-Golang {
Print-Step "Installing Go toolchain..."
$goVersion = $script:GO_DOWNLOAD_VERSION
$goUrl = "https://go.dev/dl/go$goVersion.windows-amd64.zip"
$goZip = Join-Path $env:TEMP "go$goVersion.zip"
$goRoot = "C:\Go"
Print-Info "Downloading Go $goVersion..."
try {
Invoke-WebRequest -Uri $goUrl -OutFile $goZip -UseBasicParsing
} catch {
Print-Error "Failed to download Go: $_"
return $false
}
Print-Info "Extracting Go..."
if (Test-Path $goRoot) {
Remove-Item -Path $goRoot -Recurse -Force
}
Expand-Archive -Path $goZip -DestinationPath "C:\" -Force
Remove-Item -Path $goZip -Force
# Add to PATH if not already there
$envPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
$goPath = "$goRoot\bin"
if ($envPath -notlike "*$goPath*") {
[Environment]::SetEnvironmentVariable("Path", "$envPath;$goPath", "Machine")
$env:Path = "$env:Path;$goPath"
}
# Verify installation
if (Test-GoInstalled) {
Print-Success "Go $goVersion installed successfully"
return $true
} else {
Print-Error "Go installation verification failed"
return $false
}
}
function Compile-GoServer {
Print-Step "Compiling BetterDesk Go Server..."
if (-not (Test-Path $script:GO_SERVER_SOURCE)) {
Print-Error "Go server source not found at: $script:GO_SERVER_SOURCE"
return $false
}
$currentDir = Get-Location
Set-Location $script:GO_SERVER_SOURCE
Print-Info "Running 'go mod tidy'..."
& go mod tidy 2>&1 | ForEach-Object { Write-Host " $_" }
Print-Info "Building static binary..."
$env:CGO_ENABLED = "0"
$env:GOOS = "windows"
$env:GOARCH = "amd64"
& go build -ldflags="-s -w" -o "betterdesk-server.exe" . 2>&1 | ForEach-Object { Write-Host " $_" }
Set-Location $currentDir
$outputBinary = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
if (Test-Path $outputBinary) {
$size = [math]::Round((Get-Item $outputBinary).Length / 1MB, 2)
Print-Success "Build successful: betterdesk-server.exe ($size MB)"
return $true
} else {
Print-Error "Build failed - binary not created"
return $false
}
}
#===============================================================================
# Binary Verification Functions
#===============================================================================
function Verify-BinaryChecksum {
param(
[string]$FilePath,
[string]$ExpectedHash
)
$fileName = Split-Path -Leaf $FilePath
if (-not (Test-Path $FilePath)) {
Print-Error "File not found: $FilePath"
return $false
}
Print-Info "Verifying $fileName..."
$actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToUpper()
if ($actualHash -eq $ExpectedHash.ToUpper()) {
Print-Success "$fileName`: SHA256 OK"
return $true
} else {
Print-Error "$fileName`: SHA256 MISMATCH!"
Print-Error " Expected: $ExpectedHash"
Print-Error " Got: $actualHash"
return $false
}
}
function Verify-GoBinary {
Print-Step "Verifying Go server binary..."
$goBinary = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
if (-not (Test-Path $goBinary)) {
Print-Error "Go binary not found: $goBinary"
return $false
}
# Verify it's a valid Windows executable
try {
$peHeader = [System.IO.File]::ReadAllBytes($goBinary)[0..1]
if ($peHeader[0] -eq 0x4D -and $peHeader[1] -eq 0x5A) { # MZ header
$size = [math]::Round((Get-Item $goBinary).Length / 1MB, 2)
Print-Success "Go binary valid: betterdesk-server.exe ($size MB)"
return $true
}
} catch {
Print-Error "Failed to read binary: $_"
return $false
}
Print-Error "Invalid binary format"
return $false
}
function Verify-Binaries {
Print-Step "Verifying BetterDesk binaries..."
if ($script:SKIP_VERIFY) {
Print-Warning "Verification skipped (-SkipVerify)"
return $true
}
# Check for Go binary first
$goBinary = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
if (Test-Path $goBinary) {
return Verify-GoBinary
}
# Fallback: Check legacy Rust binaries
$binSource = Join-Path $script:ScriptDir "hbbs-patch-v2"
$errors = 0
$hbbsPath = Join-Path $binSource "hbbs-windows-x86_64.exe"
$hbbrPath = Join-Path $binSource "hbbr-windows-x86_64.exe"
if (Test-Path $hbbsPath) {
if (-not (Verify-BinaryChecksum -FilePath $hbbsPath -ExpectedHash $script:HBBS_WINDOWS_X86_64_SHA256)) {
$errors++
}
}
if (Test-Path $hbbrPath) {
if (-not (Verify-BinaryChecksum -FilePath $hbbrPath -ExpectedHash $script:HBBR_WINDOWS_X86_64_SHA256)) {
$errors++
}
}
if ($errors -gt 0) {
Print-Error "Binary verification failed! $errors error(s)"
Print-Warning "Binaries may be corrupted or outdated."
if (-not $script:AUTO_MODE) {
if (-not (Confirm-Action "Continue anyway?")) {
return $false
}
} else {
return $false
}
} else {
Print-Success "All binaries verified"
}
return $true
}
#===============================================================================
# Installation Functions
#===============================================================================
function Install-Dependencies {
Print-Step "Checking dependencies..."
# Check Python
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if (-not $pythonCmd) {
Print-Warning "Python not found! Please install Python 3.8+ from python.org"
Print-Info "Download: https://www.python.org/downloads/"
if (-not $script:AUTO_MODE) {
Press-Enter
}
return $false
}
$pythonVersion = python --version 2>&1
Print-Info "Python: $pythonVersion"
# Check pip
try {
$null = python -m pip --version 2>&1
Print-Success "pip is available"
} catch {
Print-Warning "pip not found, attempting to install..."
python -m ensurepip --upgrade
}
# Install bcrypt for password hashing (used by reset-password fallback)
Print-Step "Installing Python packages..."
python -m pip install --quiet --upgrade pip
python -m pip install --quiet bcrypt requests
Print-Success "Dependencies installed"
return $true
}
#===============================================================================
# Node.js Installation Functions
#===============================================================================
function Install-NodeJs {
Print-Step "Checking Node.js installation..."
# Check if Node.js is already installed and version is sufficient
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
if ($nodeCmd) {
$nodeVersion = (node --version) -replace 'v', '' -split '\.' | Select-Object -First 1
if ([int]$nodeVersion -ge 22) {
Print-Success "Node.js v$(node --version) already installed"
return $true
} else {
Print-Warning "Node.js version $nodeVersion is too old (need 22+). Upgrading..."
}
}
Print-Step "Installing Node.js 24 LTS..."
# Try winget first (Windows 10/11)
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if ($wingetCmd) {
Print-Info "Installing via winget..."
try {
winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements --silent
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
Print-Success "Node.js installed via winget"
return $true
} catch {
Print-Warning "winget installation failed, trying alternative method..."
}
}
# Try chocolatey
$chocoCmd = Get-Command choco -ErrorAction SilentlyContinue
if ($chocoCmd) {
Print-Info "Installing via Chocolatey..."
try {
choco install nodejs-lts -y
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
Print-Success "Node.js installed via Chocolatey"
return $true
} catch {
Print-Warning "Chocolatey installation failed..."
}
}
# Manual download as last resort
Print-Warning "Automatic installation not available."
Print-Info "Please install Node.js 24 LTS manually from: https://nodejs.org/"
Print-Info "After installation, restart the script."
return $false
}
#===============================================================================
# PostgreSQL Functions
#===============================================================================
function Choose-DatabaseType {
if ($script:AUTO_MODE) {
if ($script:USE_POSTGRESQL) {
Print-Info "Auto mode: Using PostgreSQL"
} else {
Print-Info "Auto mode: Using SQLite (default)"
}
return
}
Write-Host ""
$items = @(
"SQLite`tSingle-file DB, zero setup (recommended)",
"PostgreSQL`tProduction backend with connection pooling"
)
$returns = @("1", "2")
Invoke-MenuChoose -Title "Select Database Type" -Subtitle "SQLite is recommended for most installs" -Items $items -Returns $returns
$dbChoice = $script:MENU_CHOICE
if ([string]::IsNullOrEmpty($dbChoice)) { $dbChoice = "1" }
switch ($dbChoice) {
"2" {
$script:USE_POSTGRESQL = $true
Print-Info "Selected: PostgreSQL"
Write-Host ""
$pgHost = Read-Host "PostgreSQL host [$($script:POSTGRESQL_HOST)]"
if (![string]::IsNullOrEmpty($pgHost)) { $script:POSTGRESQL_HOST = $pgHost }
$pgPort = Read-Host "PostgreSQL port [$($script:POSTGRESQL_PORT)]"
if (![string]::IsNullOrEmpty($pgPort)) { $script:POSTGRESQL_PORT = $pgPort }
$pgDb = Read-Host "PostgreSQL database [$($script:POSTGRESQL_DB)]"
if (![string]::IsNullOrEmpty($pgDb)) { $script:POSTGRESQL_DB = $pgDb }
$pgUser = Read-Host "PostgreSQL user [$($script:POSTGRESQL_USER)]"
if (![string]::IsNullOrEmpty($pgUser)) { $script:POSTGRESQL_USER = $pgUser }
$pgPass = Read-Host "PostgreSQL password (leave empty to generate)" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pgPass)
$script:POSTGRESQL_PASS = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
}
default {
$script:USE_POSTGRESQL = $false
Print-Info "Selected: SQLite"
}
}
}
function Setup-PostgreSQLDatabase {
Print-Step "Setting up PostgreSQL database for BetterDesk..."
# Generate password if not set
if ([string]::IsNullOrEmpty($script:POSTGRESQL_PASS)) {
$script:POSTGRESQL_PASS = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 16 | ForEach-Object {[char]$_})
Print-Info "Generated PostgreSQL password"
}
# Build connection URI
$script:POSTGRESQL_URI = "postgres://$($script:POSTGRESQL_USER):$($script:POSTGRESQL_PASS)@$($script:POSTGRESQL_HOST):$($script:POSTGRESQL_PORT)/$($script:POSTGRESQL_DB)?sslmode=disable"
Print-Info "PostgreSQL URI configured: postgres://$($script:POSTGRESQL_USER):****@$($script:POSTGRESQL_HOST):$($script:POSTGRESQL_PORT)/$($script:POSTGRESQL_DB)"
Print-Warning "Note: On Windows, you must set up PostgreSQL manually before installation."
Print-Info "Required PostgreSQL setup:"
Print-Info " 1. Install PostgreSQL from https://www.postgresql.org/download/windows/"
Print-Info " 2. Create user: CREATE USER $($script:POSTGRESQL_USER) WITH PASSWORD '...' CREATEDB;"
Print-Info " 3. Create database: CREATE DATABASE $($script:POSTGRESQL_DB) OWNER $($script:POSTGRESQL_USER);"
return $true
}
function Migrate-SQLiteToPostgreSQL {
Print-Step "Migrating existing SQLite data to PostgreSQL..."
$sqliteDb = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
if (-not (Test-Path $sqliteDb)) {
Print-Info "No existing SQLite database found, skipping migration"
return
}
# Find migration binary
$migrateBin = $null
$migratePaths = @(
(Join-Path $script:ScriptDir "betterdesk-server\tools\migrate\migrate.exe"),
(Join-Path $script:ScriptDir "tools\migrate\migrate.exe")
)
foreach ($path in $migratePaths) {
if (Test-Path $path) {
$migrateBin = $path
break
}
}
if (-not $migrateBin) {
Print-Warning "Migration binary not found, skipping automatic migration"
Print-Info "You can migrate manually using: M -> 3 (SQLite -> PostgreSQL)"
return
}
# Check if SQLite has data
try {
$peerCount = & sqlite3 $sqliteDb "SELECT COUNT(*) FROM peer;" 2>$null
} catch {
$peerCount = 0
}
if ($peerCount -gt 0) {
Print-Info "Found $peerCount devices in SQLite database"
if ($script:AUTO_MODE -or (Confirm-Action "Migrate existing data to PostgreSQL?")) {
Print-Step "Creating backup before migration..."
& $migrateBin -mode backup -src $sqliteDb 2>&1 | Out-Null
Print-Step "Running SQLite -> PostgreSQL migration..."
$result = & $migrateBin -mode nodejs2go -src $sqliteDb -dst $script:POSTGRESQL_URI 2>&1
if ($LASTEXITCODE -eq 0) {
Print-Success "Migration completed! $peerCount devices migrated."
} else {
Print-Warning "Migration had issues: $result"
}
}
} else {
Print-Info "SQLite database is empty, no migration needed"
}
}
function Install-NodeJsConsole {
Print-Step "Installing Node.js Web Console..."
# Install Node.js if not present
if (-not (Install-NodeJs)) {
Print-Error "Cannot proceed without Node.js"
return $false
}
# Create directory
if (-not (Test-Path $script:CONSOLE_PATH)) {
New-Item -ItemType Directory -Path $script:CONSOLE_PATH -Force | Out-Null
}
# Check for web-nodejs folder first, then web folder with server.js
$sourceFolder = $null
$webNodejsPath = Join-Path $script:ScriptDir "web-nodejs"
$webPath = Join-Path $script:ScriptDir "web"
if (Test-Path (Join-Path $webNodejsPath "server.js")) {
$sourceFolder = $webNodejsPath
Print-Info "Found Node.js console in web-nodejs/"
} elseif (Test-Path (Join-Path $webPath "server.js")) {
$sourceFolder = $webPath
Print-Info "Found Node.js console in web/"
} else {
Print-Error "Node.js web console not found!"
Print-Info "Expected: $webNodejsPath\server.js or $webPath\server.js"
return $false
}
# Copy web files (wildcard skips dotfiles — .env.example is required by merge-env.js, #166)
Copy-Item -Path "$sourceFolder\*" -Destination $script:CONSOLE_PATH -Recurse -Force
$envExampleSrc = Join-Path $sourceFolder ".env.example"
if (Test-Path $envExampleSrc) {
Copy-Item -Path $envExampleSrc -Destination (Join-Path $script:CONSOLE_PATH ".env.example") -Force
}
$versionSrc = Join-Path $script:ScriptDir "VERSION"
if (Test-Path $versionSrc) {
Copy-Item -Path $versionSrc -Destination (Join-Path $script:CONSOLE_PATH "VERSION") -Force -ErrorAction SilentlyContinue
}
# Install npm dependencies
Print-Step "Installing npm dependencies..."
Push-Location $script:CONSOLE_PATH
try {
$npmOutput = npm install --production 2>&1
$npmOutput | ForEach-Object { Write-Host "[npm] $_" }
if ($LASTEXITCODE -ne 0) {
Print-Error "npm install failed (exit code: $LASTEXITCODE)"
Print-Info "Check npm output above for details"
Pop-Location
return $false
}
# Best-effort install of node-pty for Server Management terminal (BETA).
# Optional native module — falls back to pipe spawn if build fails.
Print-Step "Installing optional node-pty (Server Management terminal - BETA)..."
$ptyOutput = npm install --no-audit --no-fund --no-save node-pty 2>&1
if ($LASTEXITCODE -eq 0) {
Print-Success "node-pty installed (real PTY available)"
} else {
Print-Warning "node-pty install failed - Server Management terminal will use pipe fallback"
$ptyOutput | Select-Object -Last 5 | ForEach-Object { Write-Host "[node-pty] $_" }
}
# Fresh install only when no existing panel state (issue #158).
$dataDir = Join-Path $script:CONSOLE_PATH "data"
if (-not (Test-Path $dataDir)) {
New-Item -ItemType Directory -Path $dataDir -Force | Out-Null
}
$envFile = Join-Path $script:CONSOLE_PATH ".env"
$authDbPath = Join-Path $dataDir "auth.db"
$isFresh = (-not (Test-Path $envFile)) -and (-not (Test-Path $authDbPath))
if ($isFresh) {
if (Test-Path $authDbPath) {
Print-Info "Removing old auth database (fresh install)..."
Remove-Item -Force -Path $authDbPath, "$authDbPath-wal", "$authDbPath-shm" -ErrorAction SilentlyContinue
}
if ($env:ADMIN_PASSWORD) {
Print-Info "Using custom admin password from ADMIN_PASSWORD env var"
}
New-Item -ItemType File -Path (Join-Path $dataDir ".force_password_update") -Force | Out-Null
} else {
Print-Info "Update mode: preserving auth database and panel passwords"
}
if (-not (Merge-ConsoleEnv -FreshInstall:$isFresh)) {
Pop-Location
return $false
}
$nodejsAdminPassword = $env:ADMIN_PASSWORD
if (-not $nodejsAdminPassword -and (Test-Path $envFile)) {
$apLine = Select-String -Path $envFile -Pattern '^DEFAULT_ADMIN_PASSWORD=' -SimpleMatch | Select-Object -First 1
$nodejsAdminPassword = if ($apLine) { ($apLine.Line -split '=', 2)[1].Trim() } else { "" }
}
if ($script:USE_POSTGRESQL) {
Print-Info "Database: PostgreSQL"
} else {
Print-Info "Database: SQLite"
}
# Persist credentials only when explicitly requested (fresh install).
if ($script:STORE_ADMIN_CREDENTIALS -and $isFresh -and $nodejsAdminPassword) {
$credsFile = Join-Path $dataDir ".admin_credentials"
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
@("Admin Username: admin", "Admin Password: $nodejsAdminPassword", "Generated by: BetterDesk installer", "Timestamp: $timestamp") | Out-File -FilePath $credsFile -Encoding UTF8
}
$script:CONSOLE_TYPE = "nodejs"
Print-Success "Node.js Web Console installed"
return $true
} catch {
Print-Error "Failed to install npm dependencies: $_"
return $false
} finally {
Pop-Location
}
}
# Install-FlaskConsole removed in v2.3.0 - Flask support deprecated
function Migrate-Console {
param(
[string]$FromType,
[string]$ToType
)
Print-Step "Migrating from $FromType to $ToType..."
# Backup existing console
$backupPath = Join-Path $script:BACKUP_DIR "console_${FromType}_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
if (-not (Test-Path $backupPath)) {
New-Item -ItemType Directory -Path $backupPath -Force | Out-Null
}
# Backup user database (auth.db) if exists
$authDb = Join-Path $script:CONSOLE_PATH "data\auth.db"
if (Test-Path $authDb) {
Copy-Item -Path $authDb -Destination $backupPath
Print-Info "Backed up user database"
}
# Backup .env if exists
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
Copy-Item -Path $envFile -Destination $backupPath
}
# Stop old console service/task
Stop-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue -Force
Stop-ScheduledTask -TaskName $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
# Remove old console specific files
$venvPath = Join-Path $script:CONSOLE_PATH "venv"
$nodeModulesPath = Join-Path $script:CONSOLE_PATH "node_modules"
if (Test-Path $venvPath) { Remove-Item -Path $venvPath -Recurse -Force }
if (Test-Path $nodeModulesPath) { Remove-Item -Path $nodeModulesPath -Recurse -Force }
Print-Success "Old $FromType console backed up to $backupPath"
}
function Install-Console {
# Always install Node.js console (Flask removed in v2.3.0)
Print-Info "Installing Node.js web console..."
# Check for existing Flask console and migrate
if (Test-Path $script:CONSOLE_PATH) {
if ((Test-Path (Join-Path $script:CONSOLE_PATH "app.py")) -and -not (Test-Path (Join-Path $script:CONSOLE_PATH "server.js"))) {
Print-Warning "Legacy Flask console detected at $($script:CONSOLE_PATH)"
if (-not $script:AUTO_MODE) {
if (Confirm-Action "Migrate from Flask to Node.js?") {
Migrate-Console -FromType "flask" -ToType "nodejs"
} else {
Print-Info "Flask is deprecated. Installing Node.js alongside..."
}
} else {
Print-Info "Auto mode: Migrating from Flask to Node.js"
Migrate-Console -FromType "flask" -ToType "nodejs"
}
}
}
return Install-NodeJsConsole
}
function Install-Binaries {
param(
[switch]$ForceRecompile
)
Print-Step "Installing BetterDesk Go Server..."
# Create directory
if (-not (Test-Path $script:RUSTDESK_PATH)) {
New-Item -ItemType Directory -Path $script:RUSTDESK_PATH -Force | Out-Null
}
# Check for Go server binary
$goBinaryPath = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
$needCompile = $false
if (-not (Test-Path $goBinaryPath)) {
$needCompile = $true
Print-Info "Pre-compiled binary not found, attempting to compile..."
} elseif ($ForceRecompile) {
# During UPDATE: check if any .go source file is newer than the binary
$binaryTime = (Get-Item $goBinaryPath).LastWriteTime
$newerSource = Get-ChildItem -Path $script:GO_SERVER_SOURCE -Filter "*.go" -Recurse |
Where-Object { $_.LastWriteTime -gt $binaryTime } |
Select-Object -First 1
if ($newerSource) {
$needCompile = $true
Print-Info "Source code updated since last build, recompiling..."
} else {
Print-Info "Binary is up-to-date with source code"
}
}
if ($needCompile) {
# Check if Go is installed
if (-not (Test-GoInstalled)) {
Print-Info "Installing Go toolchain..."
if (-not (Install-Golang)) {
Print-Error "Failed to install Go toolchain"
return $false
}
}
# Compile Go server
if (-not (Compile-GoServer)) {
Print-Error "Failed to compile Go server"
return $false
}
} else {
Print-Info "Using existing Go server binary"
}
# Verify binary
if (-not (Verify-Binaries)) {
Print-Error "Aborting installation due to verification failure"
return $false
}
# Stop services and kill processes (prevents file locking)
Print-Info "Stopping services before binary installation..."
Stop-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue -Force
Stop-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue -Force
Stop-Service -Name $script:HBBR_SERVICE -ErrorAction SilentlyContinue -Force
Stop-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $script:HBBR_SERVICE -ErrorAction SilentlyContinue
# Kill any remaining processes
Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name "hbbs" -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name "hbbr" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
# Target path
$serverTarget = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
# Verify file is not locked
if (Test-Path $serverTarget) {
try {
$stream = [System.IO.File]::Open($serverTarget, 'Open', 'ReadWrite', 'None')
$stream.Close()
} catch {
Print-Warning "File $serverTarget is still locked, waiting..."
Start-Sleep -Seconds 3
Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue | Stop-Process -Force
}
}
# Copy binary
Copy-Item -Path $goBinaryPath -Destination $serverTarget -Force
Print-Success "Installed betterdesk-server.exe (Go: signal + relay + API)"
Print-Success "BetterDesk Go Server v$script:VERSION installed"
return $true
}
function Update-EnvForTLS {
param(
[string]$CertPath,
[string]$KeyPath,
[bool]$UpdateApiUrls = $false
)
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$content = Get-Content $envFile -Raw
$content = $content -replace 'HTTPS_ENABLED=.*', 'HTTPS_ENABLED=true'
$content = $content -replace 'SSL_CERT_PATH=.*', "SSL_CERT_PATH=$CertPath"
$content = $content -replace 'SSL_KEY_PATH=.*', "SSL_KEY_PATH=$KeyPath"
# Internal Go API URLs must stay HTTP for RustDesk client compatibility.
$content = $content -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$content = $content -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
if ($content -match 'NODE_EXTRA_CA_CERTS=') {
$content = $content -replace 'NODE_EXTRA_CA_CERTS=.*', "NODE_EXTRA_CA_CERTS=$CertPath"
} else {
$content = $content.TrimEnd() + "`nNODE_EXTRA_CA_CERTS=$CertPath`n"
}
Set-Content -Path $envFile -Value $content -NoNewline
Print-Info "Updated .env with HTTPS configuration"
}
}
function Generate-SSLCertificates {
Print-Step "Generating self-signed TLS certificates..."
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
$certPath = Join-Path $sslDir "betterdesk.crt"
$keyPath = Join-Path $sslDir "betterdesk.key"
# Skip if certificates already exist
if ((Test-Path $certPath) -and (Test-Path $keyPath)) {
Print-Info "TLS certificates already exist at $sslDir"
Print-Info "Skipping certificate generation (use SSL config menu to regenerate)"
return $true
}
New-Item -ItemType Directory -Path $sslDir -Force | Out-Null
# Detect server IP for SAN
$serverIP = Get-PublicIP
# Try PowerShell native certificate generation first
try {
$cert = New-SelfSignedCertificate `
-DnsName "localhost", $serverIP `
-CertStoreLocation "Cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(3) `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-FriendlyName "BetterDesk Server" `
-TextExtension @("2.5.29.17={text}DNS=localhost&IPAddress=$serverIP&IPAddress=127.0.0.1")
# Export certificate (public)
Export-Certificate -Cert $cert -FilePath "$sslDir\betterdesk.cer" -Type CERT | Out-Null
# Export PFX then convert to PEM using openssl if available
$pfxPath = Join-Path $sslDir "betterdesk.pfx"
$securePassword = ConvertTo-SecureString -String "betterdesk-temp" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $securePassword | Out-Null
# Check if openssl is available for PEM conversion
$opensslCmd = Get-Command openssl -ErrorAction SilentlyContinue
if ($opensslCmd) {
& openssl pkcs12 -in $pfxPath -out $certPath -clcerts -nokeys -passin "pass:betterdesk-temp" 2>$null
& openssl pkcs12 -in $pfxPath -out $keyPath -nocerts -nodes -passin "pass:betterdesk-temp" 2>$null
Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue
} else {
# Keep PFX format for Windows (Go server can use it)
Print-Info "OpenSSL not found - certificate stored as PFX"
Print-Info "PFX path: $pfxPath"
}
# Clean up certificate from store
Remove-Item "Cert:\LocalMachine\My\$($cert.Thumbprint)" -ErrorAction SilentlyContinue
# Enable HTTPS in .env so Node.js console (admin panel port 5000/5443) uses TLS
Update-EnvForTLS -CertPath $certPath -KeyPath $keyPath
Print-Success "Self-signed TLS certificate generated"
Print-Info "Certificate: $sslDir"
Print-Info "SAN: DNS:localhost, IP:$serverIP, IP:127.0.0.1"
Print-Info "Valid for 3 years"
return $true
} catch {
Print-Warning "PowerShell certificate generation failed: $_"
# Fallback: try openssl if available
$opensslCmd = Get-Command openssl -ErrorAction SilentlyContinue
if ($opensslCmd) {
Print-Info "Falling back to openssl..."
try {
& openssl req -x509 -nodes -days 1095 -newkey rsa:2048 `
-keyout $keyPath `
-out $certPath `
-subj "/CN=$serverIP/O=BetterDesk/C=US" `
-addext "subjectAltName=IP:$serverIP,IP:127.0.0.1,DNS:localhost" 2>$null
if ((Test-Path $certPath) -and (Test-Path $keyPath)) {
Update-EnvForTLS -CertPath $certPath -KeyPath $keyPath
Print-Success "Self-signed TLS certificate generated (openssl)"
return $true
}
} catch {
Print-Warning "OpenSSL fallback also failed"
}
}
Print-Warning "Could not generate TLS certificates automatically"
Print-Info "Use SSL config menu (option C) to generate later"
return $false
}
}
function Set-ServiceLeastPrivilege {
param(
[string]$ServiceName,
[string]$NssmPath,
[string[]]$Paths
)
# Privilege separation: run the NSSM service under its per-service virtual
# account (NT SERVICE\<service>) instead of the default LocalSystem. Virtual
# accounts are unprivileged, auto-managed, need no password, and already hold
# the "Log on as a service" right. Skipped when -RunAsRoot is set.
if ($script:RUN_AS_ROOT) {
& $NssmPath set $ServiceName ObjectName "LocalSystem" 2>$null | Out-Null
return
}
$account = "NT SERVICE\$ServiceName"
& $NssmPath set $ServiceName ObjectName $account "" 2>$null | Out-Null
foreach ($p in $Paths) {
if ($p -and (Test-Path $p)) {
try {
& icacls "$p" /grant "${account}:(OI)(CI)M" /T /C /Q 2>$null | Out-Null
} catch {
Print-Warning "Could not grant $account access to $p"
}
}
}
Print-Info "Service $ServiceName runs under least-privilege account ($account)"
}
# Safe in-place patch of NSSM services (TLS API flags, HTTP URLs) without remove+install.
function Patch-ServiceDefinitions {
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if (-not $nssm) {
$nssmLocal = Join-Path $script:ScriptDir "tools\nssm.exe"
if (Test-Path $nssmLocal) { $nssm = $nssmLocal } else { return }
}
$nssmExe = if ($nssm -is [System.Management.Automation.ApplicationInfo]) { $nssm.Source } else { $nssm }
$changed = $false
foreach ($svc in @($script:SERVER_SERVICE, $script:CONSOLE_SERVICE)) {
if (-not (Get-Service -Name $svc -ErrorAction SilentlyContinue)) { continue }
try {
if ($svc -eq $script:SERVER_SERVICE) {
$args = (& $nssmExe get $svc AppParameters 2>$null)
if ($args) {
$clean = ($args -replace '\s-tls-api(=\S+)?', '' -replace '\s-tls-api-port(=\S+)?', '').Trim()
if ($clean -ne $args.Trim()) {
& $nssmExe set $svc AppParameters $clean | Out-Null
Print-Info "Patched $svc AppParameters (removed incompatible TLS API flags)"
$changed = $true
}
}
}
$envRaw = (& $nssmExe get $svc AppEnvironmentExtra 2>$null)
if ($envRaw) {
$cleanEnv = $envRaw `
-replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost' `
-replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
if ($cleanEnv -ne $envRaw) {
& $nssmExe set $svc AppEnvironmentExtra $cleanEnv | Out-Null
Print-Info "Patched $svc AppEnvironmentExtra (Go API URLs stay HTTP)"
$changed = $true
}
}
} catch { }
}
if ($changed) {
Print-Success "Service definitions patched (custom NSSM settings preserved)"
}
}
# During UPDATE: create missing services; patch existing; optional full recreate.
function Maybe-UpdateServices {
param(
[ValidateSet('default', 'recreate')]
[string]$Mode = 'default'
)
$needSetup = $false
if (-not (Get-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue)) {
$needSetup = $true
}
if ((Test-Path (Join-Path $script:CONSOLE_PATH "server.js")) -and
-not (Get-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue)) {
$needSetup = $true
}
if ($needSetup) {
Print-Info "Services missing — creating Windows services..."
Setup-Services
return
}
Patch-ServiceDefinitions
if ($Mode -eq 'recreate' -or $env:UPDATE_REFRESH_SERVICES -eq 'true') {
Print-Info "Recreating Windows services from template..."
Setup-Services
return
}
Print-Info "Services present — patched in place (Repair → Repair services for full recreate)"
}
function Setup-Services {
Print-Step "Configuring Windows services..."
# SAFETY NET: Re-read database config from .env if script vars are empty.
# This prevents PostgreSQL -> SQLite regression during UPDATE/REPAIR
# if Preserve-DatabaseConfig was not called or vars were lost.
if (-not $script:USE_POSTGRESQL) {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$dtLine = Select-String -Path $envFile -Pattern '^DB_TYPE=' -SimpleMatch | Select-Object -First 1
$_envDbType = if ($dtLine) { ($dtLine.Line -split '=', 2)[1].Trim() } else { "" }
if ($_envDbType -eq "postgres") {
$duLine = Select-String -Path $envFile -Pattern '^DATABASE_URL=' -SimpleMatch | Select-Object -First 1
$_envDbUrl = if ($duLine) { ($duLine.Line -split '=', 2)[1].Trim() } else { "" }
if ($_envDbUrl) {
$script:USE_POSTGRESQL = $true
$script:POSTGRESQL_URI = $_envDbUrl
Print-Info "Recovered PostgreSQL config from existing .env"
}
}
}
}
# Resolve relay server IP according to RELAY_MODE / RELAY_SERVERS
# Interactive relay mode selection (skipped in auto mode or when explicitly set)
if (-not $script:AUTO_MODE -and -not $script:RELAY_SERVERS -and $script:RELAY_MODE -eq "auto") {
$localIp = Get-LocalIP
Write-Host ""
Print-Info "Relay server address controls how clients connect for remote sessions."
Write-Host " 1) Internet / public (auto-detect public IP - default)" -ForegroundColor Cyan
Write-Host " 2) LAN only (use this server's local IP: $localIp)" -ForegroundColor Cyan
Write-Host " 3) Custom address (enter a specific IP or host)" -ForegroundColor Cyan
$relayChoice = Read-Host " Select relay mode [1]"
switch ($relayChoice) {
"2" { $script:RELAY_MODE = "local" }
"3" { $script:RELAY_SERVERS = Read-Host " Enter relay address (IP or host[:port])" }
default { $script:RELAY_MODE = "auto" }
}
Write-Host ""
}
$serverIP = Resolve-RelayIp
$connModeEnv = Resolve-ConnectionModeEnv
Print-Info "Relay server IP: $serverIP (mode: $(if ($script:RELAY_SERVERS) { 'fixed' } else { $script:RELAY_MODE }))"
Print-Info "API Port: $script:API_PORT"
# Build database value (raw). The DSN is passed to the Go server through NSSM
# AppEnvironmentExtra (DB_URL env var), never as a CLI argument, so the
# PostgreSQL password does not appear in the process command line.
$dbValue = ""
if ($script:USE_POSTGRESQL -and $script:POSTGRESQL_URI) {
$dbValue = $script:POSTGRESQL_URI
Print-Info "Database: PostgreSQL"
} else {
$dbValue = $script:DB_PATH
Print-Info "Database: SQLite"
}
# Check for NSSM (Non-Sucking Service Manager)
$nssmPath = Get-Command nssm -ErrorAction SilentlyContinue
if (-not $nssmPath) {
# Try to find NSSM in the project directory
$nssmLocalPath = Join-Path $script:ScriptDir "tools\nssm.exe"
if (Test-Path $nssmLocalPath) {
$nssmPath = $nssmLocalPath
} else {
Print-Warning "NSSM not found. Services will be created as scheduled tasks."
Print-Info "For proper Windows services, install NSSM from https://nssm.cc"
# Create scheduled tasks as fallback
Setup-ScheduledTasks -ServerIP $serverIP
return
}
}
$nssm = if ($nssmPath -is [System.Management.Automation.ApplicationInfo]) { $nssmPath.Source } else { $nssmPath }
# Remove legacy services
& $nssm stop $script:HBBS_SERVICE 2>$null
& $nssm remove $script:HBBS_SERVICE confirm 2>$null
& $nssm stop $script:HBBR_SERVICE 2>$null
& $nssm remove $script:HBBR_SERVICE confirm 2>$null
& $nssm stop $script:SERVER_SERVICE 2>$null
& $nssm remove $script:SERVER_SERVICE confirm 2>$null
& $nssm stop $script:CONSOLE_SERVICE 2>$null
& $nssm remove $script:CONSOLE_SERVICE confirm 2>$null
# Remove legacy Flask API service (deprecated in v2.3.0)
& $nssm stop "BetterDeskAPI" 2>$null
& $nssm remove "BetterDeskAPI" confirm 2>$null
Start-Sleep -Seconds 2
# Generate shared API key for Node.js <-> Go server communication
$apiKeyPath = Join-Path $script:RUSTDESK_PATH ".api_key"
if (-not (Test-Path $apiKeyPath)) {
$apiKeyBytes = New-Object byte[] 32
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$rng.GetBytes($apiKeyBytes)
$rng.Dispose()
$apiKey = [System.BitConverter]::ToString($apiKeyBytes) -replace '-', '' | ForEach-Object { $_.ToLower() }
Set-Content -Path $apiKeyPath -Value $apiKey -NoNewline
Print-Info "Generated API key for console-server communication"
}
# BetterDesk Go Server (single binary: signal + relay + API)
$serverExe = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
$signalRateLimit = if ($env:SIGNAL_RATE_LIMIT_PER_IP) { $env:SIGNAL_RATE_LIMIT_PER_IP } else { "20" }
if ($signalRateLimit -notmatch '^\d+$') {
Print-Warning "Invalid SIGNAL_RATE_LIMIT_PER_IP='$signalRateLimit'; using 20"
$signalRateLimit = "20"
}
$serverArgs = "-mode all -relay-servers $serverIP -key-file `"$script:RUSTDESK_PATH\id_ed25519`" -api-port $script:API_PORT -signal-rate-limit-per-ip $signalRateLimit"
# Discover admin password to sync the Go server initial admin with the
# Node.js console. It is passed via NSSM AppEnvironmentExtra (INIT_ADMIN_PASS),
# never as a CLI argument, to keep it out of the process command line.
$adminPass = $null
$credsFile = Join-Path $script:CONSOLE_PATH "data\.admin_credentials"
if (Test-Path $credsFile) {
$credsContent = Get-Content $credsFile -Raw
if ($credsContent -match ':(.+)') {
$adminPass = $Matches[1].Trim()
}
}
if (-not $adminPass) {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$line = Get-Content $envFile | Where-Object { $_ -like 'DEFAULT_ADMIN_PASSWORD=*' } | Select-Object -First 1
if ($line) {
$adminPass = ($line -split '=', 2)[1].Trim()
}
}
}
# Add TLS flags if certificates exist
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
$certPath = Join-Path $sslDir "betterdesk.crt"
$keyPath = Join-Path $sslDir "betterdesk.key"
$apiScheme = "http"
$tlsIsSelfSigned = $false
if ((Test-Path $certPath) -and (Test-Path $keyPath)) {
# Check if certificate is self-signed
try {
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath)
$tlsIsSelfSigned = ($cert.Issuer -eq $cert.Subject) -or ($cert.Subject -like "*O=BetterDesk*")
$cert.Dispose()
} catch {
$tlsIsSelfSigned = $true
}
# Enable TLS on signal/relay for client encryption.
# API port (21121) MUST stay HTTP -- RustDesk desktop clients always send
# plain HTTP to signal_port-2 and do not support HTTPS for API endpoints.
$serverArgs += " -tls-cert `"$certPath`" -tls-key `"$keyPath`" -tls-signal -tls-relay"
# RustDesk clients always send HTTP to API port. API stays HTTP for all cert types.
if (-not $tlsIsSelfSigned) {
$apiScheme = "http"
Print-Info "TLS: Enabled for signal/relay (proper certificate, API stays HTTP)"
} else {
$apiScheme = "http"
Print-Info "TLS: Enabled for signal/relay (self-signed cert, API stays HTTP)"
}
} else {
Print-Info "TLS: Disabled (no certificate found)"
}
& $nssm install $script:SERVER_SERVICE $serverExe $serverArgs
& $nssm set $script:SERVER_SERVICE AppDirectory $script:RUSTDESK_PATH
& $nssm set $script:SERVER_SERVICE DisplayName "BetterDesk Go Server v$script:VERSION"
& $nssm set $script:SERVER_SERVICE Description "BetterDesk Go Server (Signal + Relay + API)"
& $nssm set $script:SERVER_SERVICE Start SERVICE_AUTO_START
& $nssm set $script:SERVER_SERVICE AppStdout "$script:RUSTDESK_PATH\logs\server.log"
& $nssm set $script:SERVER_SERVICE AppStderr "$script:RUSTDESK_PATH\logs\server_error.log"
# Server secrets via environment (DB_URL / INIT_ADMIN_PASS) instead of CLI
# arguments, so the PostgreSQL and admin passwords stay out of the process
# command line (NSSM stores these in the ACL-protected service registry key).
$serverEnvExtra = @("DB_URL=$dbValue")
if ($adminPass) { $serverEnvExtra += "INIT_ADMIN_PASS=$adminPass" }
if ($connModeEnv) { $serverEnvExtra += $connModeEnv }
# New installs default to "managed" enrollment so stock RustDesk clients are
# queued for operator approval. Existing installs are left untouched.
if ($script:FRESH_INSTALL) { $serverEnvExtra += "ENROLLMENT_MODE=managed" }
$serverEnvExtra += "MESH_ENABLED=Y"
& $nssm set $script:SERVER_SERVICE AppEnvironmentExtra $serverEnvExtra
# Privilege separation: drop the Go server to its low-privilege virtual account.
Set-ServiceLeastPrivilege -ServiceName $script:SERVER_SERVICE -NssmPath $nssm -Paths @($script:RUSTDESK_PATH)
Print-Success "Created BetterDesk Go Server service"
# Console Service (Web Interface) - Node.js only
if ($script:CONSOLE_TYPE -eq "nodejs") {
$nodeExe = (Get-Command node -ErrorAction SilentlyContinue).Source
if (-not $nodeExe) { $nodeExe = "node.exe" }
$serverJs = Join-Path $script:CONSOLE_PATH "server.js"
& $nssm install $script:CONSOLE_SERVICE $nodeExe $serverJs
& $nssm set $script:CONSOLE_SERVICE AppDirectory $script:CONSOLE_PATH
& $nssm set $script:CONSOLE_SERVICE DisplayName "BetterDesk Web Console (Node.js)"
& $nssm set $script:CONSOLE_SERVICE Description "BetterDesk Web Management Console - Node.js"
& $nssm set $script:CONSOLE_SERVICE Start SERVICE_AUTO_START
$envExtra = @(
"NODE_ENV=production",
"RUSTDESK_DIR=$script:RUSTDESK_PATH",
"RUSTDESK_PATH=$script:RUSTDESK_PATH",
"KEYS_PATH=$script:RUSTDESK_PATH",
"DATA_DIR=$script:CONSOLE_PATH\data",
"DB_PATH=$script:RUSTDESK_PATH\db_v2.sqlite3",
"PUB_KEY_PATH=$script:RUSTDESK_PATH\id_ed25519.pub",
"API_KEY_PATH=$script:RUSTDESK_PATH\.api_key",
"HBBS_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
"BETTERDESK_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
"SERVER_BACKEND=betterdesk",
"PORT=5000",
"HOST=0.0.0.0",
"API_HOST=0.0.0.0"
)
# Propagate database type to NSSM environment
if ($script:USE_POSTGRESQL -and $script:POSTGRESQL_URI) {
$envExtra += "DB_TYPE=postgres"
$envExtra += "DATABASE_URL=$($script:POSTGRESQL_URI)"
} else {
$envExtra += "DB_TYPE=sqlite"
}
# Enable HTTPS on Node.js console when TLS certs are available (for browser access)
# This is separate from Go API TLS -- the web panel can serve HTTPS for browsers
if ((Test-Path $certPath) -and (Test-Path $keyPath)) {
$envExtra += "HTTPS_ENABLED=true"
$envExtra += "SSL_CERT_PATH=$certPath"
$envExtra += "SSL_KEY_PATH=$keyPath"
$envExtra += "RUSTDESK_API_TLS=$(if ($tlsIsSelfSigned) { 'false' } else { 'auto' })"
}
# Trust self-signed cert for localhost API communication
if ($tlsIsSelfSigned -and (Test-Path $certPath)) {
$envExtra += "NODE_EXTRA_CA_CERTS=$certPath"
}
& $nssm set $script:CONSOLE_SERVICE AppEnvironmentExtra $envExtra
& $nssm set $script:CONSOLE_SERVICE AppStdout "$script:CONSOLE_PATH\logs\console.log"
& $nssm set $script:CONSOLE_SERVICE AppStderr "$script:CONSOLE_PATH\logs\console_error.log"
# Privilege separation: the console's virtual account needs read/write on
# its own dir and read access to the server keys / API key in RUSTDESK_PATH.
Set-ServiceLeastPrivilege -ServiceName $script:CONSOLE_SERVICE -NssmPath $nssm -Paths @($script:CONSOLE_PATH, $script:RUSTDESK_PATH)
Print-Success "Created Node.js console service"
}
# Create logs directories
New-Item -ItemType Directory -Path "$script:RUSTDESK_PATH\logs" -Force | Out-Null
New-Item -ItemType Directory -Path "$script:CONSOLE_PATH\logs" -Force | Out-Null
Print-Success "Windows services configured"
Print-Info "Services: $script:SERVER_SERVICE, $script:CONSOLE_SERVICE"
}
function Get-GoBillingEnvLauncherLines {
param([string]$EnvFilePath)
$lines = @()
$keys = @(
'NTP_SERVERS',
'BILLING_MAX_CLOCK_SKEW_MS',
'BILLING_REQUIRE_SYNCED_CLOCK',
'BILLING_TRUST_OS_NTP'
)
if (-not $EnvFilePath -or -not (Test-Path $EnvFilePath)) {
return $lines
}
foreach ($rawLine in Get-Content $EnvFilePath) {
$line = $rawLine.Trim()
if (-not $line -or $line.StartsWith('#')) { continue }
$parts = $line -split '=', 2
if ($parts.Count -lt 2) { continue }
$key = $parts[0].Trim()
if ($keys -notcontains $key) { continue }
$value = $parts[1].Trim()
$lines += "set `"$key=$value`""
}
return $lines
}
function Setup-ScheduledTasks {
param([string]$ServerIP)
Print-Step "Creating scheduled tasks as service alternative..."
# Build database value (raw). Injected into the launcher below as an env var,
# never as a task action argument (those are visible in Task Scheduler).
$dbValue = ""
if ($script:USE_POSTGRESQL -and $script:POSTGRESQL_URI) {
$dbValue = $script:POSTGRESQL_URI
} else {
$dbValue = $script:DB_PATH
}
# Remove existing tasks
Unregister-ScheduledTask -TaskName $script:SERVER_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:HBBS_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:HBBR_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:CONSOLE_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
# BetterDesk Go Server Task
$serverExe = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
$signalRateLimit = if ($env:SIGNAL_RATE_LIMIT_PER_IP) { $env:SIGNAL_RATE_LIMIT_PER_IP } else { "20" }
if ($signalRateLimit -notmatch '^\d+$') {
Print-Warning "Invalid SIGNAL_RATE_LIMIT_PER_IP='$signalRateLimit'; using 20"
$signalRateLimit = "20"
}
$serverArgs = "-mode all -relay-servers $ServerIP -key-file `"$script:RUSTDESK_PATH\id_ed25519`" -api-port $script:API_PORT -signal-rate-limit-per-ip $signalRateLimit"
# Discover admin password (synced with Node.js console). Injected via the
# protected launcher as INIT_ADMIN_PASS, never as a task action argument.
$adminPass = $null
$credsFile = Join-Path $script:CONSOLE_PATH "data\.admin_credentials"
if (Test-Path $credsFile) {
$credsContent = Get-Content $credsFile -Raw
if ($credsContent -match ':(.+)') {
$adminPass = $Matches[1].Trim()
}
}
if (-not $adminPass) {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$line = Get-Content $envFile | Where-Object { $_ -like 'DEFAULT_ADMIN_PASSWORD=*' } | Select-Object -First 1
if ($line) {
$adminPass = ($line -split '=', 2)[1].Trim()
}
}
}
# Add TLS flags if certificates exist
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
$certPath = Join-Path $sslDir "betterdesk.crt"
$keyPath = Join-Path $sslDir "betterdesk.key"
$tlsIsSelfSigned = $false
if ((Test-Path $certPath) -and (Test-Path $keyPath)) {
try {
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath)
$tlsIsSelfSigned = ($cert.Issuer -eq $cert.Subject) -or ($cert.Subject -like "*O=BetterDesk*")
$cert.Dispose()
} catch {
$tlsIsSelfSigned = $true
}
$serverArgs += " -tls-cert `"$certPath`" -tls-key `"$keyPath`" -tls-signal -tls-relay"
if (-not $tlsIsSelfSigned) {
Print-Info "TLS: Enabled for signal/relay (proper certificate, API stays HTTP)"
} else {
Print-Info "TLS: Enabled for signal/relay (self-signed, API stays HTTP)"
}
}
# Secrets (DB_URL / INIT_ADMIN_PASS) are injected via a launcher script that
# is restricted to Administrators/SYSTEM, never via the task action arguments
# (which are visible in Task Scheduler and the process command line).
$serverLauncher = Join-Path $script:RUSTDESK_PATH "start-betterdesk-server.cmd"
$launcherLines = @("@echo off")
$launcherLines += "set `"DB_URL=$dbValue`""
if ($adminPass) { $launcherLines += "set `"INIT_ADMIN_PASS=$adminPass`"" }
# New installs default to "managed" enrollment; existing installs untouched.
if ($script:FRESH_INSTALL) { $launcherLines += "set `"ENROLLMENT_MODE=managed`"" }
$billingEnvFile = Join-Path $script:CONSOLE_PATH ".env"
foreach ($billingLine in (Get-GoBillingEnvLauncherLines -EnvFilePath $billingEnvFile)) {
$launcherLines += $billingLine
}
$launcherLines += "`"$serverExe`" $serverArgs"
Set-Content -Path $serverLauncher -Value $launcherLines -Encoding ASCII
& icacls $serverLauncher /inheritance:r /grant:r "*S-1-5-32-544:F" "*S-1-5-18:F" 2>$null | Out-Null
$serverAction = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$serverLauncher`"" -WorkingDirectory $script:RUSTDESK_PATH
$serverTrigger = New-ScheduledTaskTrigger -AtStartup
$serverPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$serverSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName $script:SERVER_SERVICE -Action $serverAction -Trigger $serverTrigger -Principal $serverPrincipal -Settings $serverSettings -Description "BetterDesk Go Server (Signal + Relay + API)" | Out-Null
# Console Task - Node.js
if ($script:CONSOLE_TYPE -eq "nodejs") {
$nodeExe = (Get-Command node -ErrorAction SilentlyContinue).Source
if (-not $nodeExe) { $nodeExe = "node.exe" }
$serverJs = Join-Path $script:CONSOLE_PATH "server.js"
$consoleAction = New-ScheduledTaskAction -Execute $nodeExe -Argument $serverJs -WorkingDirectory $script:CONSOLE_PATH
$consoleDesc = "BetterDesk Web Console (Node.js)"
Print-Info "Creating Node.js console task"
}
$consoleTrigger = New-ScheduledTaskTrigger -AtStartup
$consolePrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$consoleSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName $script:CONSOLE_SERVICE -Action $consoleAction -Trigger $consoleTrigger -Principal $consolePrincipal -Settings $consoleSettings -Description $consoleDesc | Out-Null
Print-Success "Scheduled tasks created"
}
function Run-Migrations {
Print-Step "Running database migrations..."
# Ensure database directory exists
$dbDir = Split-Path -Parent $script:DB_PATH
if (-not (Test-Path $dbDir)) {
New-Item -ItemType Directory -Path $dbDir -Force | Out-Null
}
# Create database schema and add missing columns
$pythonScript = @"
import sqlite3
import os
from datetime import datetime
db_path = r'$($script:DB_PATH)'
# Ensure db directory exists
os.makedirs(os.path.dirname(db_path), exist_ok=True)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Create peer table if not exists
cursor.execute('''
CREATE TABLE IF NOT EXISTS peer (
guid BLOB PRIMARY KEY NOT NULL,
id VARCHAR(100) NOT NULL,
uuid BLOB NOT NULL,
pk BLOB NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
user BLOB,
status INTEGER DEFAULT 0,
note VARCHAR(300),
info TEXT NOT NULL,
last_online TEXT,
is_deleted INTEGER DEFAULT 0,
deleted_at TEXT,
updated_at TEXT,
previous_ids TEXT,
id_changed_at TEXT,
is_banned INTEGER DEFAULT 0
)
''')
# Create indexes
cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS index_peer_id ON peer (id)')
cursor.execute('CREATE INDEX IF NOT EXISTS index_peer_user ON peer (user)')
cursor.execute('CREATE INDEX IF NOT EXISTS index_peer_created_at ON peer (created_at)')
cursor.execute('CREATE INDEX IF NOT EXISTS index_peer_status ON peer (status)')
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
is_active INTEGER NOT NULL DEFAULT 1
)
''')
# Create sessions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(64) PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
last_activity DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
''')
# Create audit_log table
cursor.execute('''
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action VARCHAR(50) NOT NULL,
device_id VARCHAR(100),
details TEXT,
ip_address VARCHAR(50),
timestamp DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
)
''')
# Create indexes for auth tables
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_device ON audit_log(device_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp)')
# Add missing columns to peer table
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'),
('is_banned', 'INTEGER DEFAULT 0'),
]
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 migrations completed")
"@
$pythonScript | python
Print-Success "Migrations completed"
}
function Create-AdminUser {
Print-Step "Creating admin user..."
# Detect console type
$currentConsoleType = ""
if (Test-Path (Join-Path $script:CONSOLE_PATH "server.js")) {
$currentConsoleType = "nodejs"
} elseif (Test-Path (Join-Path $script:CONSOLE_PATH "app.py")) {
$currentConsoleType = "nodejs" # Legacy Flask detected, treat as Node.js
Print-Warning "Legacy Flask console detected. Please migrate to Node.js."
} else {
Print-Warning "No console detected, skipping admin creation"
return $null
}
# Node.js console - admin is created automatically on startup.
# Prefer plaintext credentials file (legacy), then .env fallback.
$adminPassword = $null
$dataDir = Join-Path $script:CONSOLE_PATH "data"
$credsFile = Join-Path $dataDir ".admin_credentials"
if (Test-Path $credsFile) {
$creds = Get-Content $credsFile -Raw
$adminPassword = ($creds -split ':')[1].Trim()
}
if (-not $adminPassword) {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$line = Get-Content $envFile | Where-Object { $_ -like 'DEFAULT_ADMIN_PASSWORD=*' } | Select-Object -First 1
if ($line) {
$adminPassword = ($line -split '=', 2)[1].Trim()
}
}
}
if ($adminPassword) {
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " PANEL LOGIN CREDENTIALS " -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Login: " -NoNewline; Write-Host "admin" -ForegroundColor White
Write-Host " Password: " -NoNewline; Write-Host $adminPassword -ForegroundColor White
Write-Host "============================================================" -ForegroundColor Green
Write-Host ""
if ($script:STORE_ADMIN_CREDENTIALS) {
# Legacy behavior (opt-in): persist plaintext credentials file
$mainCredsFile = Join-Path $script:RUSTDESK_PATH ".admin_credentials"
"admin:$adminPassword" | Out-File -FilePath $mainCredsFile -Encoding UTF8
Print-Info "Credentials saved in: $mainCredsFile"
} else {
Print-Warning "Credentials are not persisted by default (security hardening)."
}
return $adminPassword
} else {
Print-Warning "No Node.js admin credentials found"
Print-Info "Use password reset option to set a new admin password"
return $null
}
}
function Start-Services {
Print-Step "Starting services..."
# Try Go server service first, then legacy
$goServiceExists = Get-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
$legacyServiceExists = Get-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue
if ($goServiceExists) {
# New Go single-binary architecture
Start-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
Start-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
} elseif ($legacyServiceExists) {
# Legacy Rust architecture (hbbs + hbbr)
Start-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Start-Service -Name $script:HBBR_SERVICE -ErrorAction SilentlyContinue
Start-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
} else {
# Try scheduled tasks (Go first, then legacy)
$goTaskExists = Get-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
if ($goTaskExists) {
Start-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
} else {
Start-ScheduledTask -TaskName $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Start-ScheduledTask -TaskName $script:HBBR_SERVICE -ErrorAction SilentlyContinue
}
Start-ScheduledTask -TaskName $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 3
Detect-Installation
if ($script:SERVER_RUNNING -or ($script:HBBS_RUNNING -and $script:HBBR_RUNNING)) {
Print-Success "All services started"
} else {
Print-Warning "Some services may not be working properly"
Print-Info "Check logs in: $script:RUSTDESK_PATH\logs\"
}
}
function Stop-AllServices {
Print-Step "Stopping services..."
# Stop Windows services (Go server + legacy)
Stop-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue -Force
Stop-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue -Force
Stop-Service -Name $script:HBBR_SERVICE -ErrorAction SilentlyContinue -Force
Stop-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue -Force
# Stop scheduled tasks
Stop-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $script:HBBR_SERVICE -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
# Kill processes directly (Go server + legacy)
Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name "hbbs" -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name "hbbr" -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object {
$_.MainModule.FileName -like "*betterdesk*" -or $_.CommandLine -like "*server.js*"
} | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
#===============================================================================
# Enhanced Service Management Functions (v2.1.2)
#===============================================================================
function Test-PortAvailable {
param([int]$Port, [string]$ServiceName = "unknown")
$listener = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
if ($listener) {
$process = Get-Process -Id $listener.OwningProcess -ErrorAction SilentlyContinue
Print-Error "Port $Port is in use by: $($process.Name) (PID: $($listener.OwningProcess))"
return $false
}
return $true
}
function Test-ServiceHealth {
param(
[string]$ServiceName,
[int]$ExpectedPort = 0,
[int]$TimeoutSeconds = 10
)
# Check if process is running
$processName = if ($ServiceName -eq $script:SERVER_SERVICE) { "betterdesk-server" }
elseif ($ServiceName -match "Signal") { "hbbs" }
elseif ($ServiceName -match "Relay") { "hbbr" }
elseif ($ServiceName -eq $script:CONSOLE_SERVICE) { "node" }
else { "betterdesk-server" }
$process = Get-Process -Name $processName -ErrorAction SilentlyContinue
if (-not $process) {
Print-Error "Process $processName is not running"
return $false
}
# Check port if specified
if ($ExpectedPort -gt 0) {
$elapsed = 0
while ($elapsed -lt $TimeoutSeconds) {
$listener = Get-NetTCPConnection -LocalPort $ExpectedPort -State Listen -ErrorAction SilentlyContinue
if ($listener) {
return $true
}
Start-Sleep -Seconds 1
$elapsed++
}
Print-Error "Service not listening on port $ExpectedPort after ${TimeoutSeconds}s"
return $false
}
return $true
}
function Test-HttpEndpoint {
param(
[Parameter(Mandatory = $true)][string]$Url,
[int]$TimeoutSeconds = 30
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
try {
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 3
# A 3xx response is valid for a panel configured to redirect HTTP
# to HTTPS; the listener is reachable and the operator can use
# the protocol-specific check from the installer menu.
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) {
return $true
}
} catch {
# The service may still be warming up; retry until the deadline.
}
Start-Sleep -Seconds 1
} while ((Get-Date) -lt $deadline)
Print-Error "HTTP health check failed: $Url"
return $false
}
function Start-ServicesWithVerification {
Print-Step "Starting services with health verification..."
$hasErrors = $false
# Check ports first
if (-not (Test-PortAvailable -Port 21116 -ServiceName "betterdesk-server")) {
Print-Error "Port 21116 (ID server) not available"
$hasErrors = $true
}
if (-not (Test-PortAvailable -Port 21117 -ServiceName "betterdesk-server")) {
Print-Error "Port 21117 (relay) not available"
$hasErrors = $true
}
if ($hasErrors) {
Print-Error "Cannot start services - ports in use"
Print-Info "Use: Get-NetTCPConnection -State Listen | Where-Object LocalPort -in 21116,21117"
return $false
}
# Start Go Server (single binary: signal + relay + API)
Print-Info "Starting $($script:SERVER_SERVICE) (Go server)..."
$goServiceExists = Get-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
if ($goServiceExists) {
Start-Service -Name $script:SERVER_SERVICE -ErrorAction SilentlyContinue
} else {
# Try scheduled task
$goTaskExists = Get-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
if ($goTaskExists) {
Start-ScheduledTask -TaskName $script:SERVER_SERVICE -ErrorAction SilentlyContinue
} else {
# Legacy fallback: start hbbs + hbbr separately
Print-Warning "Go server service not found, trying legacy hbbs/hbbr..."
$legacyService = Get-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue
if ($legacyService) {
Start-Service -Name $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Start-Service -Name $script:HBBR_SERVICE -ErrorAction SilentlyContinue
} else {
Start-ScheduledTask -TaskName $script:HBBS_SERVICE -ErrorAction SilentlyContinue
Start-ScheduledTask -TaskName $script:HBBR_SERVICE -ErrorAction SilentlyContinue
}
}
}
Start-Sleep -Seconds 3
if (-not (Test-ServiceHealth -ServiceName $script:SERVER_SERVICE -ExpectedPort 21116 -TimeoutSeconds 10)) {
Print-Error "Failed to start BetterDesk server"
return $false
}
Print-Success "BetterDesk server started and healthy (signal + relay + API)"
# Inject shared API key into Go server database for Node.js <-> Go communication
$apiKeyPath = Join-Path $script:RUSTDESK_PATH ".api_key"
$goDbPath = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
if ((Test-Path $apiKeyPath) -and (Test-Path $goDbPath)) {
$apiKey = Get-Content $apiKeyPath -Raw
$apiKey = $apiKey.Trim()
try {
$env:BETTERDESK_GO_DB_PATH = $goDbPath
$env:BETTERDESK_API_KEY_TMP = $apiKey
$pythonScript = "import os, sqlite3; conn = sqlite3.connect(os.environ['BETTERDESK_GO_DB_PATH']); conn.execute('INSERT OR REPLACE INTO server_config (key, value) VALUES (?, ?)', ('api_key', os.environ['BETTERDESK_API_KEY_TMP'])); conn.commit(); conn.close()"
python -c $pythonScript 2>$null
Remove-Item Env:BETTERDESK_GO_DB_PATH -ErrorAction SilentlyContinue
Remove-Item Env:BETTERDESK_API_KEY_TMP -ErrorAction SilentlyContinue
if ($LASTEXITCODE -eq 0) {
Print-Info "API key synced to Go server database"
}
} catch {
# Non-critical: API key sync failed, Node.js will still work with JWT auth
}
}
# Start Console
Print-Info "Starting $($script:CONSOLE_SERVICE)..."
$consoleService = Get-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
if ($consoleService) {
Start-Service -Name $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
} else {
Start-ScheduledTask -TaskName $script:CONSOLE_SERVICE -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 2
$healthOk = $true
if (-not (Test-HttpEndpoint -Url "http://127.0.0.1:$($script:GO_API_PORT)/api/health")) {
$healthOk = $false
}
if (-not (Test-HttpEndpoint -Url "http://127.0.0.1:5000/health")) {
$healthOk = $false
}
$protocolScript = Join-Path $script:ScriptDir "scripts\installer-protocol-check.js"
$node = Get-Command node -ErrorAction SilentlyContinue
if ($node -and (Test-Path $protocolScript)) {
& $node.Source $protocolScript `
--api-url "http://127.0.0.1:$($script:GO_API_PORT)/api/health" `
--panel-url "http://127.0.0.1:5000/health" `
--port "127.0.0.1:21116" | ForEach-Object { Print-Info "$_" }
if ($LASTEXITCODE -ne 0) {
$healthOk = $false
}
}
if (-not $healthOk) {
Print-Error "Services are running but HTTP health verification failed"
return $false
}
Print-Success "All services started and verified"
return $true
}
#=============================================================================
# Minimal Installation Function (Go server only, no web console)
#===============================================================================
function Do-InstallMinimal {
Print-Header
Write-Host "========== MINIMAL INSTALLATION (Server Only) ==========" -ForegroundColor White
Write-Host ""
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 $script:API_PORT or TCP admin console."
Write-Host ""
Detect-Installation
if ($script:INSTALL_STATUS -eq "complete") {
Print-Warning "BetterDesk is already installed!"
if (-not $script:AUTO_MODE) {
if (-not (Confirm-Action "Do you want to reinstall in Minimal mode?")) {
return
}
}
Do-BackupSilent
}
# Choose database type (SQLite or PostgreSQL)
Choose-DatabaseType
# Gracefully stop existing services
Graceful-StopServices
# Create installation directory
$installDir = $script:INSTALL_DIR
if (-not (Test-Path $installDir)) {
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
}
# Setup PostgreSQL if selected
if ($script:USE_POSTGRESQL) {
if (-not (Setup-PostgreSQLDatabase)) {
Print-Error "PostgreSQL setup failed"
return
}
}
# Install Go server binary
Detect-Architecture
if (-not (Install-Binaries)) {
Print-Error "Binary installation failed"
return
}
# Skip console installation entirely
Print-Info "Skipping web console (Minimal mode)"
# Generate self-signed TLS certificates
Generate-SSLCertificates
# Setup only the Go server service (no console service)
Setup-ServicesMinimal
# Configure firewall rules (server ports only)
Print-Step "Configuring firewall rules..."
$ports = @([int]$script:GO_API_PORT, 21115, 21116, 21117, 21118, 21119)
foreach ($port in $ports) {
try {
New-NetFirewallRule -DisplayName "BetterDesk Port $port" -Direction Inbound -LocalPort $port -Protocol TCP -Action Allow -ErrorAction SilentlyContinue | Out-Null
} catch {}
}
# UDP for signal port
try {
New-NetFirewallRule -DisplayName "BetterDesk Signal UDP 21116" -Direction Inbound -LocalPort 21116 -Protocol UDP -Action Allow -ErrorAction SilentlyContinue | Out-Null
} catch {}
# Start server
Print-Step "Starting BetterDesk server..."
$svcName = "BetterDeskServer"
if (Get-Service $svcName -ErrorAction SilentlyContinue) {
Start-Service $svcName -ErrorAction SilentlyContinue
} elseif (Get-Command nssm -ErrorAction SilentlyContinue) {
nssm start $svcName 2>$null
}
Start-Sleep -Seconds 3
# Verify
$svc = Get-Service $svcName -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq "Running") {
Print-Success "BetterDesk server is running"
} else {
Print-Warning "BetterDesk server may not have started correctly"
}
Write-Host ""
Print-Success "===== BETTERDESK MINIMAL INSTALLATION COMPLETE ====="
Write-Host ""
$serverIP = Get-PublicIP
Write-Host "Server: $serverIP" -ForegroundColor Green
Write-Host "API: http://${serverIP}:$($script:API_PORT)" -ForegroundColor Green
Write-Host ""
Write-Host "Ports: $($script:API_PORT) (API), 21115-21117 (Signal/Relay), 21118-21119 (WS)" -ForegroundColor Yellow
Write-Host "No web console installed. Use REST API or TCP admin for management." -ForegroundColor Yellow
Write-Host ""
Press-Enter
}
function Setup-ServicesMinimal {
Print-Step "Setting up BetterDesk server service (Minimal mode)..."
$goBinary = Join-Path $script:INSTALL_DIR "betterdesk-server.exe"
$keyDir = $script:INSTALL_DIR
$dbDir = $script:INSTALL_DIR
# Build arguments
$serverArgs = "-key `"$keyDir`" -db `"$dbDir`""
# Add relay servers (honors RELAY_MODE / RelayServers)
$serverIP = Resolve-RelayIp
if ($serverIP) {
$serverArgs += " -relay-servers $serverIP"
}
# TLS configuration
$tlsCert = Join-Path $script:INSTALL_DIR "cert.pem"
$tlsKey = Join-Path $script:INSTALL_DIR "key.pem"
if ((Test-Path $tlsCert) -and (Test-Path $tlsKey)) {
$serverArgs += " -tls-cert `"$tlsCert`" -tls-key `"$tlsKey`" -tls-signal -tls-relay"
}
# Remove old services
foreach ($oldSvc in @("RustDeskSignal", "RustDeskRelay", "BetterDeskAPI", "BetterDeskGo", "BetterDeskConsole")) {
if (Get-Service $oldSvc -ErrorAction SilentlyContinue) {
Stop-Service $oldSvc -Force -ErrorAction SilentlyContinue
if (Get-Command nssm -ErrorAction SilentlyContinue) {
nssm remove $oldSvc confirm 2>$null
} else {
sc.exe delete $oldSvc 2>$null
}
}
}
# Install NSSM if not present
if (-not (Get-Command nssm -ErrorAction SilentlyContinue)) {
Install-NSSM
}
# Create server service via NSSM
$svcName = "BetterDeskServer"
if (Get-Service $svcName -ErrorAction SilentlyContinue) {
nssm remove $svcName confirm 2>$null
}
nssm install $svcName $goBinary $serverArgs
nssm set $svcName AppDirectory $script:INSTALL_DIR
nssm set $svcName DisplayName "BetterDesk Server (Minimal)"
nssm set $svcName Description "BetterDesk Go server - signal, relay, and API"
nssm set $svcName Start SERVICE_AUTO_START
nssm set $svcName AppStdout (Join-Path $script:INSTALL_DIR "server.log")
nssm set $svcName AppStderr (Join-Path $script:INSTALL_DIR "server-error.log")
nssm set $svcName AppRotateFiles 1
nssm set $svcName AppRotateBytes 10485760
# Database environment
$envExtra = "SIGNAL_PORT=21116"
if ($script:USE_POSTGRESQL -and $script:POSTGRESQL_URI) {
$envExtra += "`nDB_URL=$($script:POSTGRESQL_URI)"
}
nssm set $svcName AppEnvironmentExtra $envExtra
# Privilege separation: drop the Go server to its low-privilege virtual account.
Set-ServiceLeastPrivilege -ServiceName $svcName -NssmPath "nssm" -Paths @($script:INSTALL_DIR)
Print-Success "BetterDesk server service created (Minimal mode)"
}
#=============================================================================
# Main Installation Function
#===============================================================================
function Do-Install {
Print-Header
Write-Host "========== FRESH INSTALLATION ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
if ($script:INSTALL_STATUS -eq "complete") {
Print-Warning "BetterDesk is already installed!"
if (-not $script:AUTO_MODE) {
if (-not (Confirm-Action "Do you want to reinstall?")) {
return
}
}
Do-BackupSilent
}
# Treat as fresh only when no database exists yet. Reinstalls over an
# existing database preserve the operator's current enrollment policy.
$script:FRESH_INSTALL = -not $script:DATABASE_OK
Write-Host ""
Print-Info "Starting BetterDesk Console v$script:VERSION installation..."
Write-Host ""
# Choose database type (SQLite or PostgreSQL)
Choose-DatabaseType
if (-not (Install-Dependencies)) { return }
# Setup PostgreSQL if selected
if ($script:USE_POSTGRESQL) {
if (-not (Setup-PostgreSQLDatabase)) {
Print-Error "PostgreSQL setup failed"
return
}
}
if (-not (Install-Binaries)) { Print-Error "Binary installation failed"; return }
if (-not (Install-Console)) { Print-Error "Console installation failed"; return }
# Generate self-signed TLS certificates (default for fresh installs)
Generate-SSLCertificates
# Migrate existing SQLite data to PostgreSQL if applicable
if ($script:USE_POSTGRESQL) {
Migrate-SQLiteToPostgreSQL
}
Setup-Services
Run-Migrations
$adminPassword = Create-AdminUser
# Configure firewall rules
Print-Step "Configuring Windows Firewall rules..."
Configure-Firewall | Out-Null
Start-Services
Write-Host ""
Print-Success "Installation completed successfully!"
Write-Host ""
$serverIP = Get-PublicIP
$publicKey = ""
$pubKeyPath = Join-Path $script:RUSTDESK_PATH "id_ed25519.pub"
if (Test-Path $pubKeyPath) {
$publicKey = (Get-Content $pubKeyPath -Raw).Trim()
}
$dbTypeInfo = "SQLite"
if ($script:USE_POSTGRESQL) { $dbTypeInfo = "PostgreSQL" }
$tlsStatus = "Disabled"
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
if ((Test-Path (Join-Path $sslDir "betterdesk.crt")) -and (Test-Path (Join-Path $sslDir "betterdesk.key"))) {
$tlsStatus = "Self-signed (auto-generated)"
}
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " INSTALLATION INFO " -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Panel Web: " -NoNewline; Write-Host "http://${serverIP}:5000" -ForegroundColor White
Write-Host " API Port: " -NoNewline; Write-Host $script:API_PORT -ForegroundColor White
Write-Host " Server ID: " -NoNewline; Write-Host $serverIP -ForegroundColor White
Write-Host " Database: " -NoNewline; Write-Host $dbTypeInfo -ForegroundColor White
Write-Host " TLS: " -NoNewline; Write-Host $tlsStatus -ForegroundColor White
if ($publicKey) {
Write-Host " Key: " -NoNewline; Write-Host "$($publicKey.Substring(0, [Math]::Min(20, $publicKey.Length)))..." -ForegroundColor White
}
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host " Required ports (ensure firewall allows):" -ForegroundColor Yellow
Write-Host " TCP/UDP 21116 - ID Server (client registration)"
Write-Host " TCP 21115 - NAT type test"
Write-Host " TCP 21117 - Relay Server"
Write-Host " TCP $($script:GO_API_PORT) - Go API (default, direct)"
Write-Host " TCP 5000 - Web Console (admin panel)"
Write-Host " TCP $($script:CLIENT_API_PORT) - RustDesk client API (backward-compat proxy)"
Write-Host ""
Write-Host " RustDesk Client Configuration:" -ForegroundColor Yellow
Write-Host " ID Server: $serverIP"
Write-Host " Relay Server: $serverIP"
if ($publicKey) {
Write-Host " Key: $publicKey"
}
Write-Host ""
# Auto-configure firewall rules
Write-Host " Configuring Windows Firewall rules..." -ForegroundColor Cyan
Configure-Firewall
Write-Host ""
# Offer HTTPS Enterprise configuration for fresh installs
if (-not $script:AUTO_MODE) {
Write-Host ""
Print-Info "Enterprise TLS enables HTTPS for panel/signal/relay; Go API stays HTTP for compatibility"
Print-Info "Recommended for production deployments behind trusted operator access"
Write-Host ""
if (Confirm-Action "Would you like to configure HTTPS Enterprise now? (Option 5 in SSL menu)") {
Do-ConfigureSSL
}
}
if (-not $script:AUTO_MODE) {
Press-Enter
}
}
#===============================================================================
# Update Functions
#===============================================================================
# GitHub repository configuration for online updates
$script:UPDATE_GITHUB_OWNER = if ($env:UPDATE_GITHUB_OWNER) { $env:UPDATE_GITHUB_OWNER } else { "UNITRONIX" }
$script:UPDATE_GITHUB_REPO = if ($env:UPDATE_GITHUB_REPO) { $env:UPDATE_GITHUB_REPO } else { "BetterDesk" }
$script:UPDATE_GITHUB_BRANCH = if ($env:UPDATE_GITHUB_BRANCH) { $env:UPDATE_GITHUB_BRANCH } else { "main" }
function Read-UpdateGitHubBranchFromEnv {
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if ($script:CONSOLE_PATH -and (Test-Path $envFile)) {
$line = Get-Content $envFile -ErrorAction SilentlyContinue |
Where-Object { $_ -match '^\s*UPDATE_GITHUB_BRANCH=' } |
Select-Object -Last 1
if ($line -match '^\s*UPDATE_GITHUB_BRANCH=(.+)$') {
$script:UPDATE_GITHUB_BRANCH = $Matches[1].Trim().Trim('"')
$env:UPDATE_GITHUB_BRANCH = $script:UPDATE_GITHUB_BRANCH
}
}
}
function Resolve-UpdateRemoteSha {
param([Parameter(Mandatory = $true)][string]$CloneDir)
$remoteSha = ""
$git = Get-Command git -ErrorAction SilentlyContinue
if ($git -and (Test-Path (Join-Path $CloneDir ".git"))) {
$remoteSha = ((& git -C $CloneDir rev-parse HEAD 2>$null) | Select-Object -First 1).Trim()
}
if ($remoteSha -notmatch '^[0-9a-fA-F]{40}$' -and $git) {
$remoteSha = ((& git ls-remote `
"https://github.com/$($script:UPDATE_GITHUB_OWNER)/$($script:UPDATE_GITHUB_REPO).git" `
"refs/heads/$($script:UPDATE_GITHUB_BRANCH)" 2>$null) |
Select-Object -First 1)
if ($remoteSha -is [array]) { $remoteSha = $remoteSha[0] }
if ($remoteSha) { $remoteSha = ($remoteSha -split '\s+')[0] }
}
if ($remoteSha -notmatch '^[0-9a-fA-F]{40}$') {
try {
$encodedBranch = [Uri]::EscapeDataString($script:UPDATE_GITHUB_BRANCH)
$apiUrl = "https://api.github.com/repos/$($script:UPDATE_GITHUB_OWNER)/$($script:UPDATE_GITHUB_REPO)/commits?sha=$encodedBranch&per_page=1"
$commit = Invoke-RestMethod -Uri $apiUrl -Headers @{ Accept = "application/vnd.github+json" } -TimeoutSec 30
$firstCommit = if ($commit -is [array]) { $commit[0] } else { $commit }
$remoteSha = [string]$firstCommit.sha
} catch {
$remoteSha = ""
}
}
if ($remoteSha -match '^[0-9a-fA-F]{40}$') {
return $remoteSha
}
return $null
}
function Stage-SupportAgentSource {
param([Parameter(Mandatory = $true)][string]$CloneDir)
$base = Join-Path $script:CONSOLE_PATH "agent-source"
$sources = @(
@{ Name = "betterdesk-support-agent"; Required = "build.sh" },
@{ Name = "betterdesk-agent"; Required = "go.mod" },
@{ Name = "betterdesk-server"; Required = "go.mod" }
)
$staged = 0
foreach ($item in $sources) {
$source = Join-Path $CloneDir $item.Name
$destination = Join-Path $base $item.Name
if (-not (Test-Path (Join-Path $source $item.Required))) {
Print-Warning "Support-agent source missing: $source"
continue
}
New-Item -ItemType Directory -Path $base -Force | Out-Null
Remove-Item -Path $destination -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $destination -Force | Out-Null
Get-ChildItem -Path $source -Force |
Where-Object { $_.Name -notin @(".git", "dist", "data") } |
Copy-Item -Destination $destination -Recurse -Force
$staged++
}
return ($staged -gt 0)
}
function Write-UpdateGitHubBranchToEnv {
param([Parameter(Mandatory = $true)][ValidateSet('main', 'dev')][string]$Branch)
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (-not $script:CONSOLE_PATH) {
Print-Error "Console path unknown — cannot save update channel"
return $false
}
if (-not (Test-Path $envFile)) {
New-Item -ItemType File -Path $envFile -Force | Out-Null
}
$lines = @(Get-Content $envFile -ErrorAction SilentlyContinue)
$found = $false
$updated = foreach ($line in $lines) {
if ($line -match '^\s*UPDATE_GITHUB_BRANCH=') {
$found = $true
"UPDATE_GITHUB_BRANCH=$Branch"
} else {
$line
}
}
if (-not $found) {
$updated = @($updated) + "UPDATE_GITHUB_BRANCH=$Branch"
}
Set-Content -Path $envFile -Value $updated -Encoding UTF8
$script:UPDATE_GITHUB_BRANCH = $Branch
$env:UPDATE_GITHUB_BRANCH = $Branch
Print-Success "Update channel saved (GitHub branch: $Branch)"
return $true
}
function Switch-UpdateChannel {
Print-Header
Write-Host "========== UPDATE CHANNEL ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
if ($script:INSTALL_STATUS -eq "none") {
Print-Error "BetterDesk is not installed!"
Press-Enter
return
}
Read-UpdateGitHubBranchFromEnv
Print-Info "Current GitHub branch: $($script:UPDATE_GITHUB_BRANCH)"
Write-Host ""
$items = @(
"Stable (main)`tProduction releases from the main branch",
"Development (dev)`tLatest work-in-progress from the dev branch",
"Back`tReturn without changes"
)
$returns = @("main", "dev", "0")
Invoke-MenuChoose -Title "Update Channel" -Subtitle "Stable is recommended for production servers" -Items $items -Returns $returns
switch ($script:MENU_CHOICE) {
"0" { return }
"dev" {
Print-Warning "Development channel may include unstable changes."
Write-UpdateGitHubBranchToEnv -Branch "dev" | Out-Null
}
default {
Write-UpdateGitHubBranchToEnv -Branch "main" | Out-Null
}
}
Print-Info "Run 'Check for updates' in the console or use Online GitHub update to apply."
Press-Enter
}
function Invoke-TerminalProjectUpdate {
$script:TerminalUpdateExitCode = 2
$cliPath = Join-Path $script:CONSOLE_PATH "scripts\update-cli.js"
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node -or -not (Test-Path $cliPath)) {
return
}
Print-Step "Running commit-aware project updater..."
Print-Info "Updater CLI: $cliPath"
$args = @()
if ($script:AUTO_MODE) { $args += "--yes" }
& $node.Source $cliPath @args
$script:TerminalUpdateExitCode = $LASTEXITCODE
}
# Pull latest project from GitHub and apply update to local installation.
# Downloads latest code, rebuilds Go server, reinstalls Node.js console.
# All local state (databases, keys, .env, auth.db) is preserved.
function Update-FromGitHub {
$cloneDir = Join-Path $env:TEMP "betterdesk-update-$PID"
$script:ServerBuildFailed = $false
$previousGoSource = ""
$remoteSha = ""
Read-UpdateGitHubBranchFromEnv
# Clean up any leftover clone from a previous failed run
if (Test-Path $cloneDir) { Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue }
# ---- Step 1: Clone or download latest code ----
Print-Step "Downloading latest BetterDesk from GitHub..."
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
$downloaded = $false
if ($gitCmd) {
$repoUrl = "https://github.com/$($script:UPDATE_GITHUB_OWNER)/$($script:UPDATE_GITHUB_REPO).git"
try {
& git clone --depth 1 --single-branch --branch $script:UPDATE_GITHUB_BRANCH $repoUrl $cloneDir 2>$null
if ($LASTEXITCODE -eq 0) {
Print-Success "Repository cloned (branch: $($script:UPDATE_GITHUB_BRANCH))"
$downloaded = $true
}
} catch { }
}
if (-not $downloaded) {
# Fallback: download ZIP archive
$zipUrl = "https://github.com/$($script:UPDATE_GITHUB_OWNER)/$($script:UPDATE_GITHUB_REPO)/archive/refs/heads/$($script:UPDATE_GITHUB_BRANCH).zip"
$zipPath = Join-Path $env:TEMP "betterdesk-update-$PID.zip"
Print-Info "git not available, downloading ZIP archive..."
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($zipUrl, $zipPath)
$wc.Dispose()
New-Item -ItemType Directory -Path $cloneDir -Force | Out-Null
Expand-Archive -Path $zipPath -DestinationPath $cloneDir -Force
# GitHub ZIP extracts into a subdirectory like "BetterDesk-main/"
$subDir = Get-ChildItem -Path $cloneDir -Directory | Select-Object -First 1
if ($subDir) {
Get-ChildItem -Path $subDir.FullName | Move-Item -Destination $cloneDir -Force
Remove-Item -Path $subDir.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue
Print-Success "Source downloaded and extracted"
$downloaded = $true
} catch {
Print-Error "Download failed: $($_.Exception.Message)"
Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue
return $false
}
}
if (-not $downloaded) {
Print-Error "Failed to download source code"
return $false
}
# Validate downloaded source
$goModPath = Join-Path $cloneDir "betterdesk-server\go.mod"
$serverJsPath = Join-Path $cloneDir "web-nodejs\server.js"
if (-not (Test-Path $goModPath) -or -not (Test-Path $serverJsPath)) {
Print-Error "Downloaded source is incomplete or invalid"
Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue
return $false
}
$remoteSha = Resolve-UpdateRemoteSha -CloneDir $cloneDir
if (-not $remoteSha) {
Print-Error "Could not resolve the downloaded commit SHA; refusing an untracked update"
Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue
return $false
}
Print-Info "Downloaded commit: $($remoteSha.Substring(0, 7))"
# Read remote version
$remoteVersion = ""
$versionFile = Join-Path $cloneDir "VERSION"
if (Test-Path $versionFile) {
$remoteVersion = (Get-Content $versionFile -Raw).Trim()
}
if ($remoteVersion) {
Print-Info "Remote version: $remoteVersion"
}
# ---- Step 2: Update Go server source & compile ----
Print-Step "Updating Go server source..."
$goServerSource = $script:GO_SERVER_SOURCE
if (Test-Path $goServerSource) {
$previousGoSource = "$goServerSource.pre-update.$PID"
try {
Rename-Item -Path $goServerSource -NewName $previousGoSource -ErrorAction Stop
} catch {
$previousGoSource = ""
Print-Warning "Could not stage the previous Go source tree; update will continue in place"
}
}
$sourceDir = Join-Path $cloneDir "betterdesk-server"
# Copy the *contents* into a guaranteed-existing destination. Copying the
# directory itself would nest the new tree inside an existing
# $goServerSource if the rename above failed (e.g. a momentarily locked
# file), leaving the old inconsistent source in place and breaking
# `go build` with "undefined" errors (issue #158).
New-Item -ItemType Directory -Path $goServerSource -Force | Out-Null
Copy-Item -Path "$sourceDir\*" -Destination $goServerSource -Recurse -Force
# Restore any local data/ directory from old source
$oldDataDir = if ($previousGoSource) { Join-Path $previousGoSource "data" } else { "" }
if ($oldDataDir -and (Test-Path $oldDataDir)) {
Copy-Item -Path "$oldDataDir\*" -Destination (Join-Path $goServerSource "data") -Recurse -Force -ErrorAction SilentlyContinue
}
Print-Success "Go server source updated"
# Compile Go server
Print-Step "Building Go server..."
$goAvailable = Test-GoInstalled
if (-not $goAvailable) {
Print-Info "Installing Go toolchain..."
Install-Golang
$goAvailable = Test-GoInstalled
}
if ($goAvailable) {
if (Compile-GoServer) {
Print-Success "Go server compiled successfully"
$builtBinary = Join-Path $goServerSource "betterdesk-server.exe"
if (Test-Path $builtBinary) {
$targetBinary = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
if (Test-Path $targetBinary) {
$ts = Get-Date -Format "yyyyMMddHHmmss"
Copy-Item $targetBinary "$targetBinary.bak.$ts" -ErrorAction SilentlyContinue
}
Copy-Item $builtBinary $targetBinary -Force
Print-Success "Go server binary deployed to $($script:RUSTDESK_PATH)"
}
} 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)"
$script:ServerBuildFailed = $true
}
} else {
Print-Warning "Go toolchain not available -- server binary not updated"
Print-Info "Install Go manually from https://go.dev/dl/ and re-run update"
$script:ServerBuildFailed = $true
}
# ---- Step 3: Update Node.js console files ----
Print-Step "Updating Node.js web console..."
# Files/directories to preserve during console update
$preserveItems = @(".env", ".env.local", "data", "node_modules")
$preservedDir = Join-Path $env:TEMP "betterdesk-console-state-$PID"
New-Item -ItemType Directory -Path $preservedDir -Force | Out-Null
foreach ($item in $preserveItems) {
$src = Join-Path $script:CONSOLE_PATH $item
if (Test-Path $src) {
$dst = Join-Path $preservedDir $item
Copy-Item -Path $src -Destination $dst -Recurse -Force -ErrorAction SilentlyContinue
}
}
# Copy new console files
$consoleSrc = Join-Path $cloneDir "web-nodejs"
Copy-Item -Path "$consoleSrc\*" -Destination $script:CONSOLE_PATH -Recurse -Force
$envExampleSrc = Join-Path $consoleSrc ".env.example"
if (Test-Path $envExampleSrc) {
Copy-Item -Path $envExampleSrc -Destination (Join-Path $script:CONSOLE_PATH ".env.example") -Force
}
# Restore preserved state files
foreach ($item in $preserveItems) {
$src = Join-Path $preservedDir $item
if (Test-Path $src) {
$dst = Join-Path $script:CONSOLE_PATH $item
if (Test-Path $src -PathType Container) {
if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst -Force | Out-Null }
Copy-Item -Path "$src\*" -Destination $dst -Recurse -Force -ErrorAction SilentlyContinue
} else {
Copy-Item -Path $src -Destination $dst -Force -ErrorAction SilentlyContinue
}
}
}
Remove-Item -Path $preservedDir -Recurse -Force -ErrorAction SilentlyContinue
Print-Success "Console files updated"
# Install npm dependencies
Print-Step "Installing npm dependencies..."
Push-Location $script:CONSOLE_PATH
try {
& npm install --production --no-audit --no-fund 2>$null
if ($LASTEXITCODE -eq 0) {
Print-Success "npm dependencies installed"
} else {
Print-Warning "npm install had issues (non-critical)"
}
} catch {
Print-Warning "npm install failed (non-critical): $($_.Exception.Message)"
}
Pop-Location
# Merge any new .env keys from .env.example (preserve operator settings — issue #158)
Print-Step "Merging new .env configuration keys..."
if (-not (Merge-ConsoleEnv -FreshInstall:$false)) {
Print-Warning ".env merge skipped (merge-env.js unavailable)"
}
# ---- Step 4: Update installer scripts ----
Print-Step "Updating installer scripts..."
$scriptFiles = @(
"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"
)
$scriptsUpdated = 0
foreach ($sf in $scriptFiles) {
$src = Join-Path $cloneDir $sf
if (Test-Path $src) {
Copy-Item -Path $src -Destination (Join-Path $script:ScriptDir $sf) -Force -ErrorAction SilentlyContinue
$scriptsUpdated++
}
}
Print-Success "$scriptsUpdated installer files updated"
# Stage agent sources where the console build worker expects them. This
# keeps Windows update parity with the Linux installer and avoids a full
# repository checkout on the production host.
Print-Step "Staging support-agent source for Generator builds..."
if (Stage-SupportAgentSource -CloneDir $cloneDir) {
$dataDir = Join-Path $script:CONSOLE_PATH "data"
if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }
$pending = @{ reason = "betterdesk.ps1 update"; at = (Get-Date).ToUniversalTime().ToString("o") } |
ConvertTo-Json -Compress
Set-Content -Path (Join-Path $dataDir ".agent_rebuild_pending") -Value $pending -Encoding UTF8
Print-Info "Generator bundles will rebuild after console restart"
} else {
Print-Warning "Support-agent source staging skipped"
}
if ($script:ServerBuildFailed) {
if ($previousGoSource -and (Test-Path $previousGoSource)) {
Remove-Item -Path $goServerSource -Recurse -Force -ErrorAction SilentlyContinue
try {
Rename-Item -Path $previousGoSource -NewName $goServerSource -ErrorAction Stop
} catch {
Print-Warning "Could not restore the previous Go source tree"
}
}
Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue
Print-Error "Go server binary was not rebuilt — update incomplete for server component"
return $false
}
# Only mark the update complete after the server build/deploy succeeded.
$dataDir = Join-Path $script:CONSOLE_PATH "data"
if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }
Set-Content -Path (Join-Path $dataDir ".update_sha") -Value $remoteSha
Set-Content -Path (Join-Path $dataDir ".agent_source_sha") -Value $remoteSha
Remove-Item -Path (Join-Path $dataDir ".last_update_result.json") -Force -ErrorAction SilentlyContinue
Print-Info "SHA tracking updated: $($remoteSha.Substring(0, 7))"
if ($remoteVersion -and (Test-Path (Join-Path $cloneDir "VERSION"))) {
Copy-Item -Path (Join-Path $cloneDir "VERSION") -Destination (Join-Path $script:ScriptDir "VERSION") -Force -ErrorAction SilentlyContinue
Copy-Item -Path (Join-Path $cloneDir "VERSION") -Destination (Join-Path $script:CONSOLE_PATH "VERSION") -Force -ErrorAction SilentlyContinue
}
Remove-Item -Recurse -Force $cloneDir -ErrorAction SilentlyContinue
if ($previousGoSource) {
Remove-Item -Path $previousGoSource -Recurse -Force -ErrorAction SilentlyContinue
}
Print-Success "All project files updated from GitHub"
return $true
}
function Do-Update {
Print-Header
Write-Host "========== UPDATE ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
if ($script:INSTALL_STATUS -eq "none") {
Print-Error "BetterDesk is not installed!"
Print-Info "Use 'FRESH INSTALLATION' option"
Press-Enter
return
}
# Detect Rust -> Go upgrade (major architecture change)
if ($script:SERVER_TYPE -eq "rust") {
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."
Write-Host ""
if (-not $script:AUTO_MODE) {
if (Confirm-Action "Proceed with fresh installation (recommended)?") {
Do-Install
return
} else {
Print-Warning "Continuing with update -- legacy Rust binaries will NOT be replaced with Go server."
}
} else {
Print-Info "Auto mode: Redirecting to fresh installation for Rust -> Go migration"
Do-Install
return
}
}
# CRITICAL: Preserve database configuration before reinstalling console
# This prevents PostgreSQL -> SQLite switch during updates
Preserve-DatabaseConfig
# ---- Update method selection ----
Read-UpdateGitHubBranchFromEnv
Print-Info "GitHub update branch: $($script:UPDATE_GITHUB_BRANCH)"
if ($script:AUTO_MODE) {
Print-Info "Auto mode: using GitHub pull update"
} else {
$items = @(
"Online update from GitHub`tDownload latest code + rebuild (recommended)",
"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"
)
$returns = @("1", "2", "3", "4", "0")
Invoke-MenuChoose -Title "Update Method" -Subtitle "Online GitHub update is recommended" -Items $items -Returns $returns
$updateMethod = $script:MENU_CHOICE
if (-not $updateMethod) { $updateMethod = "1" }
switch ($updateMethod) {
"0" {
return
}
"4" {
Switch-UpdateChannel
return
}
"2" {
Invoke-TerminalProjectUpdate
if ($script:TerminalUpdateExitCode -eq 0) {
Print-Success "Online project update completed"
} elseif ($script:TerminalUpdateExitCode -ne 2) {
Print-Error "In-app update failed (exit code: $($script:TerminalUpdateExitCode))"
} else {
Print-Error "In-app updater not available (Node.js or CLI script missing)"
}
Press-Enter
return
}
"3" {
# Legacy local update path
Print-Info "Using local files from: $($script:ScriptDir)"
Print-Info "Creating backup before update..."
Do-BackupSilent
Stop-AllServices
if (-not (Install-Binaries -ForceRecompile)) { Print-Error "Binary update failed"; return }
if (-not (Install-Console)) { Print-Error "Console update failed"; return }
Run-Migrations
Maybe-UpdateServices
Create-AdminUser | Out-Null
Start-Services
Print-Success "Local update completed!"
Press-Enter
return
}
}
}
# ---- GitHub Pull Update ----
Print-Info "Creating backup before update..."
Do-BackupSilent
# Stop services before updating files
Stop-AllServices
$result = Update-FromGitHub
if (-not $result) {
Print-Error "GitHub update failed"
Print-Info "Attempting to restart services with existing files..."
Start-Services
Press-Enter
return
}
# Run database migrations
Run-Migrations
$svcMode = 'default'
if (-not $script:AUTO_MODE) {
Write-Host ""
$recreateSvc = Read-Host "Recreate Windows service definitions from installer template? [y/N]"
if ($recreateSvc -match '^(y|yes)$') { $svcMode = 'recreate' }
}
Maybe-UpdateServices -Mode $svcMode
# Informational; panel passwords live in auth.db / PostgreSQL
Create-AdminUser | Out-Null
Start-Services
Print-Success "Update completed!"
Press-Enter
}
#===============================================================================
# Repair Functions
#===============================================================================
function Do-Repair {
Print-Header
Write-Host "========== REPAIR INSTALLATION ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
# CRITICAL: Preserve database configuration before any repair operation
# This prevents PostgreSQL -> SQLite switch when regenerating service files
Preserve-DatabaseConfig
Print-Status
$items = @(
"Repair binaries`tReplace the server binary with BetterDesk",
"Repair database`tAdd any missing columns",
"Repair services`tRecreate the Windows services",
"Full repair`tDo everything above",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "3", "4", "0")
Invoke-MenuChoose -Title "Repair Installation" -Subtitle "Choose what to repair" -Items $items -Returns $returns
$choice = $script:MENU_CHOICE
switch ($choice) {
"1" { Repair-Binaries }
"2" { Repair-Database }
"3" { Repair-Services }
"4" {
Repair-Binaries
Repair-Database
Repair-Services
Print-Success "Full repair completed!"
}
"0" { return }
}
Press-Enter
}
function Repair-Binaries {
Print-Step "Repairing BetterDesk server binaries..."
# The supported installer architecture uses one Go binary. Do not gate
# repairs on hbbs-patch-v2: those legacy RustDesk artifacts are absent from
# fresh Go installations and are not needed by betterdesk-server.exe.
$goSourceDir = $script:GO_SERVER_SOURCE
$goSourceBinary = Join-Path $goSourceDir "betterdesk-server.exe"
$installedGoBinary = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
$goSourceAvailable = (Test-Path $goSourceBinary) -or (Test-Path (Join-Path $goSourceDir "go.mod"))
if ($goSourceAvailable) {
if (-not (Install-Binaries -ForceRecompile)) {
Print-Error "Failed to compile or install betterdesk-server.exe"
return
}
} elseif (Test-Path $installedGoBinary) {
# A binary-only installation can still be repaired by validating and
# restarting it. Rebuilding requires the source tree or a later update.
try {
$header = [System.IO.File]::ReadAllBytes($installedGoBinary)[0..1]
if ($header[0] -ne 0x4D -or $header[1] -ne 0x5A) {
Print-Error "Invalid Windows executable: $installedGoBinary"
return
}
} catch {
Print-Error "Unable to validate $installedGoBinary`: $($_.Exception.Message)"
return
}
Print-Info "Validated existing Go server binary (source tree not present)"
} elseif ((Test-Path (Join-Path $script:RUSTDESK_PATH "hbbs.exe")) -and
(Test-Path (Join-Path $script:RUSTDESK_PATH "hbbr.exe"))) {
Print-Warning "Legacy RustDesk binaries detected; no Go source or Go binary is available."
Print-Info "Run an update or fresh Go installation to migrate this deployment."
if (-not (Start-ServicesWithVerification)) {
Print-Error "Legacy services failed to start after repair"
return
}
Print-Success "Legacy services verified; no Go binary was changed."
return
} else {
Print-Error "No BetterDesk server binary or source tree found."
Print-Info "Run a fresh installation or update before repairing binaries."
return
}
if (-not (Start-ServicesWithVerification)) {
Print-Error "Services failed to start after binary repair"
return
}
Print-Success "BetterDesk server binaries repaired and verified!"
}
function Repair-Database {
Print-Step "Repairing database..."
Run-Migrations
Print-Success "Database repaired"
}
function Repair-Services {
Print-Step "Repairing Windows services (enhanced v2.1.2)..."
# Stop services first
Stop-AllServices
Start-Sleep -Seconds 2
# Verify binaries exist (Go server: betterdesk-server.exe, fallback: legacy hbbs.exe)
$serverBinary = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
if (-not (Test-Path $serverBinary)) {
# Fallback to legacy Rust binary name
$serverBinary = Join-Path $script:RUSTDESK_PATH "hbbs.exe"
if (-not (Test-Path $serverBinary)) {
Print-Error "betterdesk-server.exe not found at $script:RUSTDESK_PATH"
Print-Info "Run 'Repair binaries' first"
return
}
}
# Recreate services/tasks
Setup-Services
# Start with verification
if (-not (Start-ServicesWithVerification)) {
Print-Error "Services failed to start after repair"
return
}
Print-Success "Services repaired and verified!"
}
#===============================================================================
# Validation Functions
#===============================================================================
function Do-Validate {
Print-Header
Write-Host "========== INSTALLATION VALIDATION ==========" -ForegroundColor White
Write-Host ""
$errors = 0
$warnings = 0
Detect-Installation
Write-Host "Checking components..." -ForegroundColor White
Write-Host ""
# Check directories
Write-Host " RustDesk directory ($script:RUSTDESK_PATH): " -NoNewline
if (Test-Path $script:RUSTDESK_PATH) {
Write-Host "[OK]" -ForegroundColor Green
} else {
Write-Host "[X] Not found" -ForegroundColor Red
$errors++
}
Write-Host " Console directory ($script:CONSOLE_PATH): " -NoNewline
if (Test-Path $script:CONSOLE_PATH) {
Write-Host "[OK]" -ForegroundColor Green
} else {
Write-Host "[X] Not found" -ForegroundColor Red
$errors++
}
# Check binaries (Go server or legacy Rust)
Write-Host " BetterDesk Server: " -NoNewline
if (Test-Path (Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe")) {
Write-Host "[OK] (Go: signal + relay + API)" -ForegroundColor Green
} elseif ((Test-Path (Join-Path $script:RUSTDESK_PATH "hbbs.exe")) -and (Test-Path (Join-Path $script:RUSTDESK_PATH "hbbr.exe"))) {
Write-Host "[OK] (Legacy Rust)" -ForegroundColor Yellow
$warnings++
} else {
Write-Host "[X] Not found" -ForegroundColor Red
$errors++
}
# Check database (SQLite or PostgreSQL)
Write-Host " Database: " -NoNewline
$valDbType = "sqlite"
$envFilePath = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFilePath) {
$dbLine = Select-String -Path $envFilePath -Pattern '^DB_TYPE=' -SimpleMatch | Select-Object -First 1
if ($dbLine) { $valDbType = ($dbLine.Line -split '=', 2)[1].Trim() }
}
if ($valDbType -eq "postgres") {
Write-Host "[OK] (PostgreSQL)" -ForegroundColor Green
} elseif (Test-Path $script:DB_PATH) {
Write-Host "[OK] (SQLite)" -ForegroundColor Green
} else {
# Go server creates DB on first start
Write-Host "[!] Not yet created (will be created when server starts)" -ForegroundColor Yellow
$warnings++
}
# Check keys
Write-Host " Public key: " -NoNewline
$pubKeyPath = Join-Path $script:RUSTDESK_PATH "id_ed25519.pub"
if (Test-Path $pubKeyPath) {
Write-Host "[OK]" -ForegroundColor Green
} else {
Write-Host "[!] Will be generated on first start" -ForegroundColor Yellow
$warnings++
}
# Check services
Write-Host ""
Write-Host "Checking services..." -ForegroundColor White
Write-Host ""
$services = @($script:HBBS_SERVICE, $script:HBBR_SERVICE, $script:CONSOLE_SERVICE)
foreach ($service in $services) {
Write-Host " ${service}: " -NoNewline
$svc = Get-Service -Name $service -ErrorAction SilentlyContinue
if ($svc) {
if ($svc.Status -eq 'Running') {
Write-Host "[OK] Running" -ForegroundColor Green
} else {
Write-Host "[!] Not running ($($svc.Status))" -ForegroundColor Yellow
$warnings++
}
} else {
$task = Get-ScheduledTask -TaskName $service -ErrorAction SilentlyContinue
if ($task) {
if ($task.State -eq 'Running') {
Write-Host "[OK] Running (task)" -ForegroundColor Green
} else {
Write-Host "[!] Task exists but not running" -ForegroundColor Yellow
$warnings++
}
} else {
Write-Host "[X] Not found" -ForegroundColor Red
$errors++
}
}
}
# Check ports
Write-Host ""
Write-Host "Checking ports..." -ForegroundColor White
Write-Host ""
$ports = @(
@{Port=[int]$script:GO_API_PORT; Desc="Go API"; Expected="betterdesk-server"},
@{Port=[int]$script:CLIENT_API_PORT; Desc="Client API proxy"; Expected="node"},
@{Port=21115; Desc="NAT Test"; Expected="hbbs"},
@{Port=21116; Desc="ID Server"; Expected="hbbs"},
@{Port=21117; Desc="Relay"; Expected="hbbr"},
@{Port=5000; Desc="Web Console"; Expected="node"}
)
foreach ($p in $ports) {
$status = Check-PortStatus -Port $p.Port -Protocol "TCP" -ExpectedService $p.Expected
Write-Host " Port $($p.Port) ($($p.Desc)): " -NoNewline
if ($status.Listening) {
if ($status.Conflict) {
Write-Host "[!] CONFLICT - $($status.ProcessName) (PID $($status.PID))" -ForegroundColor Red
$errors++
} else {
Write-Host "[OK] $($status.ProcessName)" -ForegroundColor Green
}
} else {
Write-Host "[!] Not listening" -ForegroundColor Yellow
$warnings++
}
}
# Check firewall
Write-Host ""
Write-Host "Checking firewall..." -ForegroundColor White
Write-Host ""
$firewallProfile = Get-NetFirewallProfile -ErrorAction SilentlyContinue
$activeProfiles = $firewallProfile | Where-Object { $_.Enabled -eq $true }
if ($activeProfiles) {
$fwPorts = @(21115, 21116, 21117, 21118, 21119, 5000, 5443, [int]$script:GO_API_PORT, [int]$script:CLIENT_API_PORT)
$fwMissing = 0
foreach ($fwPort in $fwPorts) {
$rules = Get-NetFirewallRule -Direction Inbound -Enabled True -ErrorAction SilentlyContinue |
Where-Object { $_.Action -eq 'Allow' } |
Get-NetFirewallPortFilter -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -eq $fwPort }
if (-not $rules) { $fwMissing++ }
}
if ($fwMissing -gt 0) {
Write-Host " Firewall: $fwMissing rule(s) missing" -ForegroundColor Yellow
Write-Host " Use DIAGNOSTICS > F to auto-configure" -ForegroundColor Yellow
$warnings += $fwMissing
} else {
Write-Host " Firewall: All rules configured" -ForegroundColor Green
}
} else {
Write-Host " Firewall: Disabled" -ForegroundColor Green
}
# Summary
Write-Host ""
Write-Host "=======================================" -ForegroundColor White
if ($errors -eq 0 -and $warnings -eq 0) {
Write-Host "[OK] Installation correct - no problems found" -ForegroundColor Green
} elseif ($errors -eq 0) {
Write-Host "[!] Found $warnings warning(s)" -ForegroundColor Yellow
} else {
Write-Host "[X] Found $errors error(s) and $warnings warning(s)" -ForegroundColor Red
Write-Host "Use 'REPAIR INSTALLATION' option to fix problems" -ForegroundColor Cyan
}
Press-Enter
}
#===============================================================================
# Backup Functions
#===============================================================================
function Do-Backup {
Print-Header
Write-Host "========== BACKUP ==========" -ForegroundColor White
Write-Host ""
Do-BackupSilent
Print-Success "Backup completed!"
Press-Enter
}
function Do-BackupSilent {
$backupName = "betterdesk_backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
$backupPath = Join-Path $script:BACKUP_DIR $backupName
if (-not (Test-Path $script:BACKUP_DIR)) {
New-Item -ItemType Directory -Path $script:BACKUP_DIR -Force | Out-Null
}
New-Item -ItemType Directory -Path $backupPath -Force | Out-Null
Print-Step "Creating backup: $backupName"
# Backup database
if (Test-Path $script:DB_PATH) {
Copy-Item -Path $script:DB_PATH -Destination $backupPath
Print-Info " - Database"
}
# Backup keys
$keyPath = Join-Path $script:RUSTDESK_PATH "id_ed25519"
if (Test-Path $keyPath) {
Copy-Item -Path $keyPath -Destination $backupPath
Copy-Item -Path "$keyPath.pub" -Destination $backupPath -ErrorAction SilentlyContinue
Print-Info " - Keys"
}
# Backup API key
$apiKeyPath = Join-Path $script:RUSTDESK_PATH ".api_key"
if (Test-Path $apiKeyPath) {
Copy-Item -Path $apiKeyPath -Destination $backupPath
Print-Info " - API key"
}
# Backup credentials (check both locations)
$consoleCredPath = Join-Path $script:CONSOLE_PATH "data\.admin_credentials"
$rustdeskCredPath = Join-Path $script:RUSTDESK_PATH ".admin_credentials"
if (Test-Path $consoleCredPath) {
Copy-Item -Path $consoleCredPath -Destination $backupPath
Print-Info " - Login credentials"
} elseif (Test-Path $rustdeskCredPath) {
Copy-Item -Path $rustdeskCredPath -Destination $backupPath
Print-Info " - Login credentials"
}
# Create zip archive
$zipPath = "$backupPath.zip"
Compress-Archive -Path $backupPath -DestinationPath $zipPath -Force
Remove-Item -Path $backupPath -Recurse -Force
Print-Success "Backup saved: $zipPath"
}
#===============================================================================
# Password Reset Function
#===============================================================================
function Do-ResetPassword {
Print-Header
Write-Host "========== ADMIN PASSWORD RESET ==========" -ForegroundColor White
Write-Host ""
# Detect console type
Detect-Installation
if ($script:CONSOLE_TYPE -eq "none") {
Print-Error "No console installation detected"
Print-Info "Run installation first"
Press-Enter
return
}
$items = @(
"Generate random password`tCreate a new strong password",
"Set custom password`tType the password yourself",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "0")
Invoke-MenuChoose -Title "Admin Password Reset" -Subtitle "Console type: Node.js" -Items $items -Returns $returns
$choice = $script:MENU_CHOICE
$newPassword = $null
switch ($choice) {
"1" { $newPassword = Generate-RandomPassword }
"2" {
$newPassword = Read-Host "Enter new password (min 8 chars)"
if ($newPassword.Length -lt 8) {
Print-Error "Password too short!"
Press-Enter
return
}
}
"0" { return }
default { return }
}
if (-not $newPassword) { return }
$success = $false
if ($script:CONSOLE_TYPE -eq "nodejs") {
# --- Hotfix: detect broken Go-first auth flow (commit 188991d) ---
$authServicePath = Join-Path $script:CONSOLE_PATH "services\authService.js"
if (Test-Path $authServicePath) {
$authContent = Get-Content $authServicePath -Raw -ErrorAction SilentlyContinue
if ($authContent -match 'const health = await checkGoServerHealth') {
Print-Warning "Detected broken authentication flow (Go-first delegation bug)"
Print-Info "Downloading fixed authService.js from GitHub..."
$fixUrl = "https://raw.githubusercontent.com/UNITRONIX/BetterDesk/main/web-nodejs/services/authService.js"
$tmpPath = "$authServicePath.tmp"
try {
Invoke-WebRequest -Uri $fixUrl -OutFile $tmpPath -UseBasicParsing -ErrorAction Stop
$tmpContent = Get-Content $tmpPath -Raw
if ($tmpContent -match 'Step 1: Check local database FIRST') {
Move-Item -Path $tmpPath -Destination $authServicePath -Force
Print-Success "Fixed authentication flow (restored local-first login)"
} else {
Remove-Item $tmpPath -Force -ErrorAction SilentlyContinue
Print-Warning "Downloaded file does not contain expected fix - skipped"
}
} catch {
Remove-Item $tmpPath -Force -ErrorAction SilentlyContinue
Print-Warning "Could not download fix (no internet?) - password reset will proceed but login may still fail"
Print-Info "Manual fix: download $fixUrl to $authServicePath"
}
}
}
# Detect database type from console .env
$dbType = "sqlite"
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (Test-Path $envFile) {
$envContent = Get-Content $envFile -Raw -ErrorAction SilentlyContinue
if ($envContent -match '(?m)^DB_TYPE\s*=\s*(postgres|postgresql)') {
$dbType = "postgres"
}
}
Print-Info "Database type: $dbType"
# Use Node.js reset-password script (supports both SQLite and PostgreSQL)
$resetScript = Join-Path $script:CONSOLE_PATH "scripts\reset-password.js"
if (Test-Path $resetScript) {
Print-Info "Using reset-password.js script..."
$nodeExe = Get-Command "node" -ErrorAction SilentlyContinue
if ($nodeExe) {
Push-Location $script:CONSOLE_PATH
try {
$env:DATA_DIR = Join-Path $script:CONSOLE_PATH "data"
# The script reads .env for DB_TYPE and DATABASE_URL automatically
& node $resetScript $newPassword admin
if ($LASTEXITCODE -eq 0) {
$success = $true
}
} finally {
Pop-Location
}
}
}
# Fallback: direct database update
if (-not $success) {
Print-Info "Using direct database update..."
if ($dbType -eq "postgres") {
# PostgreSQL mode -- need psycopg2 or pg module
Print-Warning "PostgreSQL password reset requires Node.js. Please ensure node is installed."
Print-Info "Alternatively, run: psql DATABASE_URL -c `"UPDATE users SET password_hash='...' WHERE username='admin'`""
} else {
# SQLite mode -- update auth.db directly
$authDbPath = Join-Path $script:CONSOLE_PATH "data\auth.db"
if (-not (Test-Path $authDbPath)) {
$authDbPath = Join-Path $script:RUSTDESK_PATH "auth.db"
}
Print-Info "Auth database: $authDbPath"
$env:BETTERDESK_AUTH_DB_PATH = $authDbPath
$env:BETTERDESK_RESET_PASSWORD = $newPassword
$pythonScript = @"
import sqlite3
import bcrypt
import os
auth_db_path = os.environ.get('BETTERDESK_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('BETTERDESK_RESET_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")
"@
$output = $pythonScript | python 2>&1
Remove-Item Env:BETTERDESK_AUTH_DB_PATH -ErrorAction SilentlyContinue
Remove-Item Env:BETTERDESK_RESET_PASSWORD -ErrorAction SilentlyContinue
if ($output -match "successfully") {
$success = $true
} else {
Print-Warning "Python output: $output"
}
}
}
}
Write-Host ""
if ($success) {
Write-Host "============================================================" -ForegroundColor Green
Write-Host " NEW LOGIN CREDENTIALS " -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Login: " -NoNewline; Write-Host "admin" -ForegroundColor White
Write-Host " Password: " -NoNewline; Write-Host $newPassword -ForegroundColor White
Write-Host "============================================================" -ForegroundColor Green
if ($script:STORE_ADMIN_CREDENTIALS) {
# Legacy behavior (opt-in): persist plaintext credentials
$consoleCredsFile = Join-Path $script:CONSOLE_PATH "data\.admin_credentials"
$rustdeskCredsFile = Join-Path $script:RUSTDESK_PATH ".admin_credentials"
# Create console data directory if it doesn't exist
$consoleDataDir = Join-Path $script:CONSOLE_PATH "data"
if (-not (Test-Path $consoleDataDir)) {
New-Item -ItemType Directory -Path $consoleDataDir -Force | Out-Null
}
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
@("Admin Username: admin", "Admin Password: $newPassword", "Generated by: BetterDesk password reset", "Timestamp: $timestamp") | Out-File -FilePath $consoleCredsFile -Encoding UTF8
@("Admin Username: admin", "Admin Password: $newPassword", "Generated by: BetterDesk password reset", "Timestamp: $timestamp") | Out-File -FilePath $rustdeskCredsFile -Encoding UTF8
Print-Info "Credentials saved to: $consoleCredsFile"
} else {
Print-Warning "Credentials are not persisted by default (security hardening)."
}
} else {
Print-Error "Failed to reset password!"
Print-Info "Make sure Node.js is installed and the console is set up correctly"
}
Press-Enter
}
#===============================================================================
# Diagnostics Function
#===============================================================================
function Check-PortStatus {
param(
[int]$Port,
[string]$Protocol = "TCP",
[string]$ExpectedService = ""
)
$result = @{
Port = $Port
Protocol = $Protocol
Listening = $false
ProcessName = ""
PID = 0
Conflict = $false
}
if ($Protocol -eq "TCP") {
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
} else {
$conn = Get-NetUDPEndpoint -LocalPort $Port -ErrorAction SilentlyContinue
}
if ($conn) {
$result.Listening = $true
$result.PID = $conn[0].OwningProcess
try {
$proc = Get-Process -Id $result.PID -ErrorAction SilentlyContinue
$result.ProcessName = $proc.ProcessName
} catch { }
if ($ExpectedService -and $result.ProcessName -and
$result.ProcessName -notmatch $ExpectedService) {
$result.Conflict = $true
}
}
return $result
}
function Check-FirewallRules {
Write-Host ""
Write-Host "=== Windows Firewall ===" -ForegroundColor White
Write-Host ""
$firewallProfile = Get-NetFirewallProfile -ErrorAction SilentlyContinue
if (-not $firewallProfile) {
Print-Warning " Unable to query Windows Firewall"
return
}
$activeProfiles = $firewallProfile | Where-Object { $_.Enabled -eq $true }
if ($activeProfiles) {
$profileNames = ($activeProfiles | ForEach-Object { $_.Name }) -join ", "
Write-Host " Firewall active: $profileNames" -ForegroundColor Yellow
} else {
Write-Host " Firewall: Disabled" -ForegroundColor Green
return
}
# Check for BetterDesk firewall rules
$requiredPorts = @(
@{Port=21115; Proto="TCP"; Name="NAT Test"},
@{Port=21116; Proto="TCP"; Name="ID Server TCP"},
@{Port=21116; Proto="UDP"; Name="ID Server UDP"},
@{Port=21117; Proto="TCP"; Name="Relay Server"},
@{Port=21118; Proto="TCP"; Name="WebSocket Signal"},
@{Port=21119; Proto="TCP"; Name="WebSocket Relay"},
@{Port=5000; Proto="TCP"; Name="Web Console"},
@{Port=5443; Proto="TCP"; Name="Web Console HTTPS"},
@{Port=[int]$script:GO_API_PORT; Proto="TCP"; Name="Go API (default, direct)"},
@{Port=[int]$script:CLIENT_API_PORT; Proto="TCP"; Name="RustDesk client API (backward-compat proxy)"}
)
$missingRules = @()
foreach ($p in $requiredPorts) {
$rules = Get-NetFirewallRule -Direction Inbound -Enabled True -ErrorAction SilentlyContinue |
Where-Object { $_.Action -eq 'Allow' } |
Get-NetFirewallPortFilter -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -eq $p.Port -and ($_.Protocol -eq $p.Proto -or $_.Protocol -eq 'Any') }
if ($rules) {
Write-Host " Port $($p.Port)/$($p.Proto) ($($p.Name)): " -NoNewline
Write-Host "ALLOWED" -ForegroundColor Green
} else {
Write-Host " Port $($p.Port)/$($p.Proto) ($($p.Name)): " -NoNewline
Write-Host "NO RULE" -ForegroundColor Red
$missingRules += $p
}
}
return $missingRules
}
function Configure-Firewall {
param([array]$MissingRules = @())
if ($MissingRules.Count -eq 0) {
# Check all required ports
$requiredPorts = @(
@{Port=21115; Proto="TCP"; Name="BetterDesk NAT Test"},
@{Port=21116; Proto="TCP"; Name="BetterDesk ID Server TCP"},
@{Port=21116; Proto="UDP"; Name="BetterDesk ID Server UDP"},
@{Port=21117; Proto="TCP"; Name="BetterDesk Relay Server"},
@{Port=21118; Proto="TCP"; Name="BetterDesk WebSocket Signal"},
@{Port=21119; Proto="TCP"; Name="BetterDesk WebSocket Relay"},
@{Port=5000; Proto="TCP"; Name="BetterDesk Web Console"},
@{Port=5443; Proto="TCP"; Name="BetterDesk Console HTTPS"},
@{Port=[int]$script:GO_API_PORT; Proto="TCP"; Name="BetterDesk Go API (default)"},
@{Port=[int]$script:CLIENT_API_PORT; Proto="TCP"; Name="BetterDesk client API (compat proxy)"}
)
foreach ($p in $requiredPorts) {
$rules = Get-NetFirewallRule -Direction Inbound -Enabled True -ErrorAction SilentlyContinue |
Where-Object { $_.Action -eq 'Allow' } |
Get-NetFirewallPortFilter -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -eq $p.Port -and ($_.Protocol -eq $p.Proto -or $_.Protocol -eq 'Any') }
if (-not $rules) {
$MissingRules += $p
}
}
}
if ($MissingRules.Count -eq 0) {
Print-Success "All firewall rules are already configured"
return $true
}
Print-Info "Creating $($MissingRules.Count) missing firewall rules..."
$created = 0
foreach ($p in $MissingRules) {
$ruleName = "BetterDesk - $($p.Name)"
try {
New-NetFirewallRule -DisplayName $ruleName `
-Direction Inbound -Action Allow `
-Protocol $p.Proto -LocalPort $p.Port `
-Profile Any -ErrorAction Stop | Out-Null
Print-Success " Created rule: $ruleName (port $($p.Port)/$($p.Proto))"
$created++
} catch {
Print-Error " Failed to create rule: $ruleName - $($_.Exception.Message)"
}
}
Print-Info "$created/$($MissingRules.Count) firewall rules created"
return ($created -eq $MissingRules.Count)
}
function Do-Diagnostics {
Print-Header
Write-Host "========== DIAGNOSTICS ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
Print-Status
Write-Host ""
Write-Host "=== Process Information ===" -ForegroundColor White
Write-Host ""
$serverProc = Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue
if ($serverProc) {
Write-Host " BetterDesk Server: PID $($serverProc.Id), Memory $('{0:N0}' -f ($serverProc.WorkingSet64/1MB)) MB" -ForegroundColor Green
} else {
# Fallback: check legacy hbbs/hbbr processes
$hbbsProc = Get-Process -Name "hbbs" -ErrorAction SilentlyContinue
$hbbrProc = Get-Process -Name "hbbr" -ErrorAction SilentlyContinue
if ($hbbsProc -or $hbbrProc) {
if ($hbbsProc) {
Write-Host " HBBS (legacy): PID $($hbbsProc.Id), Memory $('{0:N0}' -f ($hbbsProc.WorkingSet64/1MB)) MB" -ForegroundColor Yellow
}
if ($hbbrProc) {
Write-Host " HBBR (legacy): PID $($hbbrProc.Id), Memory $('{0:N0}' -f ($hbbrProc.WorkingSet64/1MB)) MB" -ForegroundColor Yellow
}
Print-Warning "Legacy Rust processes detected. Consider migrating to Go server."
} else {
Write-Host " BetterDesk Server: Not running" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "=== Database Statistics ===" -ForegroundColor White
Write-Host ""
if (Test-Path $script:DB_PATH) {
$fileInfo = Get-Item $script:DB_PATH
Write-Host " Size: $('{0:N2}' -f ($fileInfo.Length/1KB)) KB"
Write-Host " Modified: $($fileInfo.LastWriteTime)"
# Get database counts
$pythonScript = @"
import sqlite3
db_path = r'$($script:DB_PATH)'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
cursor.execute("SELECT COUNT(*) FROM peer WHERE is_deleted = 0")
devices = cursor.fetchone()[0]
print(f" Devices: {devices}")
except:
print(" Devices: Unable to query")
try:
cursor.execute("SELECT COUNT(*) FROM peer WHERE status = 1 AND is_deleted = 0")
online = cursor.fetchone()[0]
print(f" Online: {online}")
except:
pass
try:
cursor.execute("SELECT COUNT(*) FROM users")
users = cursor.fetchone()[0]
print(f" Users: {users}")
except:
pass
conn.close()
"@
$pythonScript | python
} else {
Write-Host " Database does not exist"
}
# --- Port diagnostics ---
Write-Host ""
Write-Host "=== Port Diagnostics ===" -ForegroundColor White
Write-Host ""
$portDefs = @(
@{Port=[int]$script:GO_API_PORT; Proto="TCP"; Expected="betterdesk-server"; Desc="Go API (default)"},
@{Port=[int]$script:CLIENT_API_PORT; Proto="TCP"; Expected="node"; Desc="Client API (backward-compat proxy)"},
@{Port=21115; Proto="TCP"; Expected="betterdesk-server"; Desc="NAT Test"},
@{Port=21116; Proto="TCP"; Expected="betterdesk-server"; Desc="ID Server (TCP)"},
@{Port=21116; Proto="UDP"; Expected="betterdesk-server"; Desc="ID Server (UDP)"},
@{Port=21117; Proto="TCP"; Expected="betterdesk-server"; Desc="Relay Server"},
@{Port=5000; Proto="TCP"; Expected="node"; Desc="Web Console"}
)
$portIssues = 0
foreach ($pd in $portDefs) {
$status = Check-PortStatus -Port $pd.Port -Protocol $pd.Proto -ExpectedService $pd.Expected
$label = " Port $($pd.Port)/$($pd.Proto) ($($pd.Desc)):"
if ($status.Listening) {
if ($status.Conflict) {
Write-Host "$label " -NoNewline
Write-Host "CONFLICT - used by $($status.ProcessName) (PID $($status.PID))" -ForegroundColor Red
$portIssues++
} else {
Write-Host "$label " -NoNewline
Write-Host "OK - $($status.ProcessName) (PID $($status.PID))" -ForegroundColor Green
}
} else {
Write-Host "$label " -NoNewline
Write-Host "NOT LISTENING" -ForegroundColor Yellow
}
}
if ($portIssues -gt 0) {
Write-Host ""
Print-Warning "$portIssues port conflict(s) detected!"
Write-Host " Tip: Stop conflicting processes or change ports in configuration" -ForegroundColor Yellow
Write-Host " Common fix: Ensure no other app uses ports 21115-21117, 5000, $($script:GO_API_PORT), $($script:CLIENT_API_PORT)" -ForegroundColor Yellow
}
# --- Firewall diagnostics ---
$missingRules = Check-FirewallRules
if ($missingRules -and $missingRules.Count -gt 0) {
Write-Host ""
Print-Warning "$($missingRules.Count) firewall rule(s) missing!"
Write-Host " Use option 'F' from diagnostics menu to auto-configure firewall" -ForegroundColor Yellow
}
# --- API connectivity test ---
Write-Host ""
Write-Host "=== API Connectivity ===" -ForegroundColor White
Write-Host ""
$apiUrl = "http://127.0.0.1:$($script:API_PORT)/api/server-info"
try {
$response = Invoke-WebRequest -Uri $apiUrl -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
Write-Host " Server API ($($script:API_PORT)): " -NoNewline
Write-Host "OK (HTTP $($response.StatusCode))" -ForegroundColor Green
} catch {
Write-Host " Server API ($($script:API_PORT)): " -NoNewline
Write-Host "UNREACHABLE" -ForegroundColor Red
}
$consoleUrl = "http://127.0.0.1:5000/health"
try {
$response = Invoke-WebRequest -Uri $consoleUrl -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
Write-Host " Web Console (5000): " -NoNewline
Write-Host "OK (HTTP $($response.StatusCode))" -ForegroundColor Green
} catch {
Write-Host " Web Console (5000): " -NoNewline
Write-Host "UNREACHABLE" -ForegroundColor Red
}
# --- Diagnostics sub-menu ---
Write-Host ""
$items = @(
"Configure firewall rules`tAuto-create any missing rules",
"Test port connectivity`tProbe ports from outside",
"Back`tReturn to the main menu"
)
$returns = @("F", "P", "0")
Invoke-MenuChoose -Title "Diagnostics Actions" -Subtitle "Optional follow-up checks" -Items $items -Returns $returns
$subChoice = $script:MENU_CHOICE
switch ($subChoice) {
"F" {
Write-Host ""
Configure-Firewall -MissingRules $missingRules
Press-Enter
}
"P" {
Write-Host ""
Write-Host "=== External Port Test ===" -ForegroundColor White
Write-Host ""
$serverIP = Get-PublicIP
Print-Info "Public IP: $serverIP"
Print-Info "Testing external port accessibility... (this may take a moment)"
Write-Host ""
foreach ($port in @(21115, 21116, 21117)) {
Write-Host " Port ${port}: " -NoNewline
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$result = $tcp.BeginConnect($serverIP, $port, $null, $null)
$success = $result.AsyncWaitHandle.WaitOne(3000)
if ($success -and $tcp.Connected) {
Write-Host "REACHABLE" -ForegroundColor Green
} else {
Write-Host "BLOCKED/UNREACHABLE" -ForegroundColor Red
}
$tcp.Close()
} catch {
Write-Host "BLOCKED/UNREACHABLE" -ForegroundColor Red
}
}
Press-Enter
}
default { return }
}
}
#===============================================================================
# Uninstall Function
#===============================================================================
function Do-Uninstall {
Print-Header
Write-Host "========== UNINSTALL ==========" -ForegroundColor Red
Write-Host ""
Print-Warning "This operation will remove BetterDesk Console!"
Write-Host ""
if (-not $script:AUTO_MODE -and -not $script:UNINSTALL_MODE) {
if (-not (Confirm-Action "Are you sure you want to continue?")) {
return
}
}
if ($script:AUTO_MODE -or (Confirm-Action "Create backup before uninstall?")) {
Do-BackupSilent
}
Print-Step "Stopping services..."
Stop-AllServices
Print-Step "Removing services..."
# Remove Windows services (NSSM). Installations may keep NSSM beside the
# installer instead of putting it on PATH.
$nssmCommand = Get-Command nssm -ErrorAction SilentlyContinue
$nssmCandidates = @()
if ($nssmCommand) {
$nssmCandidates += if ($nssmCommand -is [System.Management.Automation.ApplicationInfo]) {
$nssmCommand.Source
} else {
[string]$nssmCommand
}
}
$nssmCandidates += Join-Path $script:ScriptDir "tools\nssm.exe"
$nssm = $nssmCandidates |
Where-Object { $_ -and (Test-Path $_) } |
Select-Object -First 1
if ($nssm) {
& $nssm remove $script:SERVER_SERVICE confirm 2>$null
& $nssm remove $script:HBBS_SERVICE confirm 2>$null
& $nssm remove $script:HBBR_SERVICE confirm 2>$null
& $nssm remove $script:CONSOLE_SERVICE confirm 2>$null
}
# Also remove services directly when NSSM is unavailable or a stale
# service definition survived an earlier uninstall.
foreach ($serviceName in @(
$script:SERVER_SERVICE,
$script:HBBS_SERVICE,
$script:HBBR_SERVICE,
$script:CONSOLE_SERVICE,
"BetterDeskAPI"
)) {
if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
sc.exe delete $serviceName 2>$null | Out-Null
}
}
# Remove scheduled tasks
Unregister-ScheduledTask -TaskName $script:SERVER_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:HBBS_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:HBBR_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $script:CONSOLE_SERVICE -Confirm:$false -ErrorAction SilentlyContinue
if ($script:PURGE_MODE -or (-not $script:AUTO_MODE -and (Confirm-Action "Remove installation files ($script:RUSTDESK_PATH)?"))) {
Remove-Item -Path $script:RUSTDESK_PATH -Recurse -Force -ErrorAction SilentlyContinue
Print-Info "Removed: $script:RUSTDESK_PATH"
} else {
Print-Info "Preserved server data: $script:RUSTDESK_PATH"
}
if ($script:PURGE_MODE -or (-not $script:AUTO_MODE -and (Confirm-Action "Remove Web Console ($script:CONSOLE_PATH)?"))) {
Remove-Item -Path $script:CONSOLE_PATH -Recurse -Force -ErrorAction SilentlyContinue
Print-Info "Removed: $script:CONSOLE_PATH"
} else {
Print-Info "Preserved console data: $script:CONSOLE_PATH"
}
Print-Success "BetterDesk has been uninstalled"
Press-Enter
}
#===============================================================================
# Path Configuration
#===============================================================================
function Configure-Paths {
Print-Header
Write-Host ""
Write-Host "=== Path Configuration ===" -ForegroundColor White
Write-Host ""
Write-Host " Current RustDesk path: " -NoNewline; Write-Host $script:RUSTDESK_PATH -ForegroundColor Cyan
Write-Host " Current Console path: " -NoNewline; Write-Host $script:CONSOLE_PATH -ForegroundColor Cyan
Write-Host " Database path: " -NoNewline; Write-Host $script:DB_PATH -ForegroundColor Cyan
Write-Host ""
$items = @(
"Auto-detect paths`tProbe for an existing installation",
"Set server path`tEnter the BetterDesk server path",
"Set console path`tEnter the web console path",
"Reset to defaults`tRestore the default paths",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "3", "4", "0")
Invoke-MenuChoose -Title "Path Configuration" -Subtitle "Server: $script:RUSTDESK_PATH" -Items $items -Returns $returns
$choice = $script:MENU_CHOICE
switch ($choice) {
"1" {
$script:RUSTDESK_PATH = ""
$script:CONSOLE_PATH = ""
Auto-DetectPaths
Press-Enter
Configure-Paths
}
"2" {
Write-Host ""
$newPath = Read-Host "Enter RustDesk server path (e.g., C:\BetterDesk)"
if ($newPath) {
if (Test-Path $newPath) {
$script:RUSTDESK_PATH = $newPath
$script:DB_PATH = "$script:RUSTDESK_PATH\db_v2.sqlite3"
Print-Success "RustDesk path set to: $script:RUSTDESK_PATH"
} else {
Print-Warning "Directory does not exist: $newPath"
if (Confirm-Action "Create this directory?") {
New-Item -ItemType Directory -Path $newPath -Force | Out-Null
$script:RUSTDESK_PATH = $newPath
$script:DB_PATH = "$script:RUSTDESK_PATH\db_v2.sqlite3"
Print-Success "Created and set RustDesk path: $script:RUSTDESK_PATH"
}
}
}
Press-Enter
Configure-Paths
}
"3" {
Write-Host ""
$newPath = Read-Host "Enter Console path (e.g., C:\BetterDeskConsole)"
if ($newPath) {
if (Test-Path $newPath) {
$script:CONSOLE_PATH = $newPath
Print-Success "Console path set to: $script:CONSOLE_PATH"
} else {
Print-Warning "Directory does not exist: $newPath"
if (Confirm-Action "Create this directory?") {
New-Item -ItemType Directory -Path $newPath -Force | Out-Null
$script:CONSOLE_PATH = $newPath
Print-Success "Created and set Console path: $script:CONSOLE_PATH"
}
}
}
Press-Enter
Configure-Paths
}
"4" {
$script:RUSTDESK_PATH = "C:\BetterDesk"
$script:CONSOLE_PATH = "C:\BetterDeskConsole"
$script:DB_PATH = "$script:RUSTDESK_PATH\db_v2.sqlite3"
Print-Success "Paths reset to defaults"
Press-Enter
Configure-Paths
}
"0" { return }
default {
Print-Error "Invalid option"
Start-Sleep -Seconds 1
Configure-Paths
}
}
}
#===============================================================================
# Build Functions
#===============================================================================
function Do-Build {
Print-Header
$items = @(
"Rebuild & deploy Go server`tCompile, stop, replace, start",
"Compile Go server only`tBuild without deploying",
"Build legacy Rust binaries`tArchived hbbs/hbbr",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "3", "0")
Invoke-MenuChoose -Title "Build & Deploy" -Subtitle "Compile the BetterDesk server" -Items $items -Returns $returns
$buildChoice = $script:MENU_CHOICE
if ([string]::IsNullOrEmpty($buildChoice)) { $buildChoice = "1" }
switch ($buildChoice) {
"1" { Do-RebuildGoServer }
"2" { Do-CompileGoOnly }
"3" { Do-BuildLegacyRust }
"0" { return }
default { Print-Warning "Invalid option"; Start-Sleep -Seconds 1 }
}
}
# Rebuild & deploy Go server: compile -> backup -> stop -> replace -> start -> verify
function Do-RebuildGoServer {
Print-Header
Write-Host "========== REBUILD & DEPLOY GO SERVER ==========" -ForegroundColor White
Write-Host ""
Detect-Installation
if ($script:INSTALL_STATUS -eq "none") {
Print-Warning "BetterDesk is not installed. Binary will be compiled but not deployed."
if (-not (Confirm-Action "Continue with compilation only?")) {
Press-Enter
return
}
Do-CompileGoOnly
return
}
# Step 1: Compile
Print-Step "[1/5] Compiling Go server from source..."
if (-not (Compile-GoServer)) {
Print-Error "Compilation failed - aborting. Current installation is untouched."
Press-Enter
return
}
$newBinary = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
if (-not (Test-Path $newBinary)) {
Print-Error "Compiled binary not found at $newBinary"
Press-Enter
return
}
# Step 2: Backup current binary
Print-Step "[2/5] Backing up current binary..."
$installedBinary = Join-Path $script:RUSTDESK_PATH "betterdesk-server.exe"
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$backupPath = "${installedBinary}.backup.${ts}"
if (Test-Path $installedBinary) {
Copy-Item -Path $installedBinary -Destination $backupPath -Force
Print-Info "Backup: $backupPath"
} else {
Print-Info "No existing binary to backup"
}
# Step 3: Stop services
Print-Step "[3/5] Stopping services..."
Stop-AllServices
# Step 4: Replace binary
Print-Step "[4/5] Deploying new binary..."
if (-not (Test-Path $script:RUSTDESK_PATH)) {
New-Item -ItemType Directory -Path $script:RUSTDESK_PATH -Force | Out-Null
}
# Verify file is not locked
if (Test-Path $installedBinary) {
try {
$stream = [System.IO.File]::Open($installedBinary, 'Open', 'ReadWrite', 'None')
$stream.Close()
} catch {
Print-Warning "File is locked, waiting..."
Start-Sleep -Seconds 3
Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
}
}
Copy-Item -Path $newBinary -Destination $installedBinary -Force
$size = [math]::Round((Get-Item $installedBinary).Length / 1MB, 2)
Print-Success "Deployed: $installedBinary ($size MB)"
# Step 5: Start services and verify
Print-Step "[5/5] Starting services..."
Start-ServicesWithVerification
# Verify
Start-Sleep -Seconds 3
$serverProcess = Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue
if ($serverProcess) {
Write-Host ""
Print-Success "Go server rebuilt and deployed successfully!"
} else {
Print-Error "Service failed to start after rebuild!"
Write-Host ""
Write-Host "Rolling back to previous binary..." -ForegroundColor Yellow
if (Test-Path $backupPath) {
# Stop again
Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Copy-Item -Path $backupPath -Destination $installedBinary -Force
Start-Services
Start-Sleep -Seconds 3
$rollbackProcess = Get-Process -Name "betterdesk-server" -ErrorAction SilentlyContinue
if ($rollbackProcess) {
Print-Success "Rollback successful - previous binary restored"
} else {
Print-Error "Rollback also failed. Check event log for details."
}
} else {
Print-Error "No backup to rollback to."
}
}
Press-Enter
}
# Compile Go server only (no deployment)
function Do-CompileGoOnly {
Print-Header
Write-Host "========== COMPILE GO SERVER ==========" -ForegroundColor White
Write-Host ""
if (-not (Compile-GoServer)) {
Print-Error "Compilation failed"
Press-Enter
return
}
$newBinary = Join-Path $script:GO_SERVER_SOURCE "betterdesk-server.exe"
$size = [math]::Round((Get-Item $newBinary).Length / 1MB, 2)
Print-Success "Binary compiled: $newBinary ($size MB)"
Print-Info "Use option 7 -> 1 to deploy it, or copy manually."
Press-Enter
}
# Legacy Rust build (archived - hbbs/hbbr)
function Do-BuildLegacyRust {
Print-Header
Write-Host "========== BUILD LEGACY RUST BINARIES ==========" -ForegroundColor White
Write-Host ""
Print-Warning "Legacy Rust binaries (hbbs/hbbr) are archived."
Print-Info "The Go server is the current architecture."
Write-Host ""
if (-not (Confirm-Action "Continue with legacy Rust build anyway?")) {
return
}
# Check Rust
$cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue
if (-not $cargoCmd) {
Print-Error "Rust is not installed!"
Print-Info "Install from: https://rustup.rs"
if (Confirm-Action "Open Rust installation page?") {
Start-Process "https://rustup.rs"
}
Press-Enter
return
}
$rustVersion = rustc --version
Print-Info "Rust: $rustVersion"
Write-Host ""
$buildDir = Join-Path $env:TEMP "betterdesk_build_$((Get-Date).Ticks)"
New-Item -ItemType Directory -Path $buildDir -Force | Out-Null
Push-Location $buildDir
try {
Print-Step "Downloading RustDesk Server sources..."
git clone --depth 1 --branch 1.1.14 https://github.com/rustdesk/rustdesk-server.git
Set-Location "rustdesk-server"
git submodule update --init --recursive
Print-Step "Applying BetterDesk modifications..."
$srcDir = Join-Path $script:ScriptDir "hbbs-patch-v2\src"
if (Test-Path $srcDir) {
Copy-Item -Path "$srcDir\main.rs" -Destination "src\main.rs" -Force
Copy-Item -Path "$srcDir\http_api.rs" -Destination "src\http_api.rs" -Force
Copy-Item -Path "$srcDir\database.rs" -Destination "src\database.rs" -Force
Copy-Item -Path "$srcDir\peer.rs" -Destination "src\peer.rs" -Force -ErrorAction SilentlyContinue
Copy-Item -Path "$srcDir\rendezvous_server.rs" -Destination "src\rendezvous_server.rs" -Force -ErrorAction SilentlyContinue
} else {
Print-Error "Source modifications not found: $srcDir"
return
}
Print-Step "Compiling (may take several minutes)..."
cargo build --release
Print-Step "Copying binaries..."
$outputDir = Join-Path $script:ScriptDir "hbbs-patch-v2"
Copy-Item -Path "target\release\hbbs.exe" -Destination "$outputDir\hbbs-windows-x86_64.exe" -Force
Copy-Item -Path "target\release\hbbr.exe" -Destination "$outputDir\hbbr-windows-x86_64.exe" -Force
Print-Success "Legacy Rust compilation completed!"
Print-Info "Binaries saved in: $outputDir"
} finally {
Pop-Location
Remove-Item -Path $buildDir -Recurse -Force -ErrorAction SilentlyContinue
}
Press-Enter
}
#===============================================================================
# SSL Certificate Configuration
#===============================================================================
function Do-ConfigureSSL {
Print-Header
Write-Host "========== SSL CERTIFICATE CONFIGURATION ==========" -ForegroundColor White
Write-Host ""
$envFile = Join-Path $script:CONSOLE_PATH ".env"
if (-not (Test-Path $envFile)) {
Print-Error "Node.js console .env not found at $envFile"
Print-Info "Please install BetterDesk first (option 1)"
Press-Enter
return
}
$items = @(
"Let's Encrypt`tACME certificate (manual on Windows)",
"Custom certificate`tProvide your own cert + key files",
"Self-signed certificate`tQuick HTTPS for testing",
"Disable SSL`tRevert the panel back to HTTP",
"Enterprise TLS`tPanel + signal + relay (API stays HTTP)",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "3", "4", "5", "0")
Invoke-MenuChoose -Title "SSL Certificate Configuration" -Subtitle "Enables HTTPS for the admin panel" -Items $items -Returns $returns
$sslChoice = $script:MENU_CHOICE
if ([string]::IsNullOrEmpty($sslChoice)) { $sslChoice = "3" }
$envContent = Get-Content $envFile -Raw
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
switch ($sslChoice) {
"0" { return }
"1" {
# Let's Encrypt
Print-Warning "Let's Encrypt is not yet supported on Windows via this script."
Print-Info "Please use Certbot manually or option 2 (custom certificate)."
Press-Enter
return
}
"2" {
# Custom certificate
Write-Host ""
$certPath = Read-Host "Path to certificate file (PEM)"
$keyPath = Read-Host "Path to private key file (PEM)"
$caPath = Read-Host "Path to CA bundle (optional, press Enter to skip)"
if (-not (Test-Path $certPath)) {
Print-Error "Certificate file not found: $certPath"
Press-Enter
return
}
if (-not (Test-Path $keyPath)) {
Print-Error "Key file not found: $keyPath"
Press-Enter
return
}
$envContent = $envContent -replace 'HTTPS_ENABLED=.*', 'HTTPS_ENABLED=true'
$envContent = $envContent -replace 'SSL_CERT_PATH=.*', "SSL_CERT_PATH=$certPath"
$envContent = $envContent -replace 'SSL_KEY_PATH=.*', "SSL_KEY_PATH=$keyPath"
if (-not [string]::IsNullOrEmpty($caPath) -and (Test-Path $caPath)) {
$envContent = $envContent -replace 'SSL_CA_PATH=.*', "SSL_CA_PATH=$caPath"
}
$envContent = $envContent -replace 'HTTP_REDIRECT_HTTPS=.*', 'HTTP_REDIRECT_HTTPS=true'
if ($envContent -match 'RUSTDESK_API_TLS=') {
$envContent = $envContent -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=true'
} else {
$envContent = $envContent.TrimEnd() + "`nRUSTDESK_API_TLS=true`n"
}
Set-Content $envFile -Value $envContent -NoNewline
Print-Success "Custom SSL certificate configured"
}
"3" {
# Self-signed with full SANs
New-Item -ItemType Directory -Path $sslDir -Force | Out-Null
$certPath = Join-Path $sslDir "betterdesk.crt"
$keyPath = Join-Path $sslDir "betterdesk.key"
Write-Host ""
$certDomain = Read-Host "Enter domain name (optional, press Enter to skip)"
# Detect IPs
$serverIp = Get-PublicIP
$lanIp = ""
try {
$lanIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -notmatch '^127\.' -and $_.IPAddress -notmatch '^169\.254\.'
} | Select-Object -First 1).IPAddress
} catch {
$lanIp = [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME) |
Where-Object { $_.AddressFamily -eq 'InterNetwork' -and $_.IPAddressToString -notmatch '^127\.' } |
Select-Object -First 1 -ExpandProperty IPAddressToString
}
# Build SAN list
$sanList = "IP:$serverIp,IP:127.0.0.1,DNS:localhost"
if ($lanIp -and $lanIp -ne $serverIp) {
$sanList = "$sanList,IP:$lanIp"
}
if ($certDomain) {
$sanList = "DNS:$certDomain,$sanList"
}
$cn = if ($certDomain) { $certDomain } else { $serverIp }
Print-Step "Generating self-signed certificate..."
Print-Info "SANs: $sanList"
# Use openssl if available, otherwise PowerShell
$openssl = Get-Command openssl -ErrorAction SilentlyContinue
if ($openssl) {
$sanArg = "subjectAltName=$sanList"
& openssl req -x509 -nodes -days 3650 -newkey rsa:2048 `
-keyout $keyPath -out $certPath `
-subj "/CN=$cn/O=BetterDesk/C=PL" `
-addext $sanArg 2>&1 | Out-Null
if (-not (Test-Path $certPath)) {
# Fallback for older openssl without -addext
& openssl req -x509 -nodes -days 3650 -newkey rsa:2048 `
-keyout $keyPath -out $certPath `
-subj "/CN=$cn/O=BetterDesk/C=PL" 2>&1 | Out-Null
}
} else {
# PowerShell self-signed cert
$dnsNames = @("localhost")
if ($certDomain) { $dnsNames += $certDomain }
$cert = New-SelfSignedCertificate -DnsName $dnsNames `
-CertStoreLocation "cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(10) `
-KeyExportPolicy Exportable
# Export as PFX then convert to PEM via openssl if available
$pfxPath = Join-Path $sslDir "betterdesk.pfx"
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
[System.IO.File]::WriteAllBytes($pfxPath, $certBytes)
Print-Warning "Generated PFX certificate at $pfxPath"
Print-Warning "For full PEM support, install OpenSSL for Windows."
$certPath = $pfxPath
$keyPath = $pfxPath
}
$envContent = $envContent -replace 'HTTPS_ENABLED=.*', 'HTTPS_ENABLED=true'
$envContent = $envContent -replace 'SSL_CERT_PATH=.*', "SSL_CERT_PATH=$certPath"
$envContent = $envContent -replace 'SSL_KEY_PATH=.*', "SSL_KEY_PATH=$keyPath"
$envContent = $envContent -replace 'HTTP_REDIRECT_HTTPS=.*', 'HTTP_REDIRECT_HTTPS=true'
if ($envContent -match 'RUSTDESK_API_TLS=') {
$envContent = $envContent -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=false'
} else {
$envContent = $envContent.TrimEnd() + "`nRUSTDESK_API_TLS=false`n"
}
# Configure NODE_EXTRA_CA_CERTS for self-signed
if ($envContent -match 'NODE_EXTRA_CA_CERTS=') {
$envContent = $envContent -replace 'NODE_EXTRA_CA_CERTS=.*', "NODE_EXTRA_CA_CERTS=$certPath"
} else {
$envContent = $envContent.TrimEnd() + "`nNODE_EXTRA_CA_CERTS=$certPath`n"
}
Set-Content $envFile -Value $envContent -NoNewline
# Configure Go server with TLS for signal/relay
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
# Remove old TLS args
$goArgs = $goArgs -replace ' -tls-cert [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-key [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-signal', ''
$goArgs = $goArgs -replace ' -tls-relay', ''
$goArgs = $goArgs -replace ' -tls-api', ''
# Add new TLS args
$goArgs = "$goArgs -tls-cert $certPath -tls-key $keyPath -tls-signal -tls-relay"
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
Print-Success "Self-signed certificate generated (valid 10 years)"
Print-Info "Certificate: $certPath"
if ($lanIp -and $lanIp -ne $serverIp) {
Print-Info "LAN IP included: $lanIp"
}
Print-Warning "Browsers will show security warning. Use Let's Encrypt for public servers."
}
"4" {
# Disable SSL
$envContent = $envContent -replace 'HTTPS_ENABLED=.*', 'HTTPS_ENABLED=false'
$envContent = $envContent -replace 'SSL_CERT_PATH=.*', 'SSL_CERT_PATH='
$envContent = $envContent -replace 'SSL_KEY_PATH=.*', 'SSL_KEY_PATH='
$envContent = $envContent -replace 'HTTP_REDIRECT_HTTPS=.*', 'HTTP_REDIRECT_HTTPS=false'
if ($envContent -match 'RUSTDESK_API_TLS=') {
$envContent = $envContent -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=false'
} else {
$envContent = $envContent.TrimEnd() + "`nRUSTDESK_API_TLS=false`n"
}
# Remove TLS args from Go server
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
$goArgs = $goArgs -replace ' -tls-cert [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-key [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-signal', ''
$goArgs = $goArgs -replace ' -tls-relay', ''
$goArgs = $goArgs -replace ' -tls-api', ''
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
Set-Content $envFile -Value $envContent -NoNewline
Print-Success "SSL disabled. Running in HTTP mode."
}
"5" {
# Enterprise TLS - HTTPS for panel/signal/relay, Go API remains HTTP
Print-Header
Write-Host "========== ENTERPRISE TLS CONFIGURATION ==========" -ForegroundColor Yellow
Write-Host ""
Write-Host " WARNING: Go API :$($script:GO_API_PORT) and compat :$($script:CLIENT_API_PORT) stay HTTP for RustDesk clients." -ForegroundColor Yellow
Write-Host " Panel, signal and relay channels can still use TLS." -ForegroundColor Yellow
Write-Host ""
New-Item -ItemType Directory -Path $sslDir -Force | Out-Null
$certPath = Join-Path $sslDir "betterdesk.crt"
$keyPath = Join-Path $sslDir "betterdesk.key"
$certDomain = Read-Host "Enter domain name (optional, press Enter to skip)"
# Detect IPs
$serverIp = Get-PublicIP
$lanIp = ""
try {
$lanIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -notmatch '^127\.' -and $_.IPAddress -notmatch '^169\.254\.'
} | Select-Object -First 1).IPAddress
} catch { }
# Build comprehensive SAN list
$sanList = "IP:$serverIp,IP:127.0.0.1,DNS:localhost"
if ($lanIp -and $lanIp -ne $serverIp) {
$sanList = "$sanList,IP:$lanIp"
}
if ($certDomain) {
$sanList = "DNS:$certDomain,$sanList"
}
$cn = if ($certDomain) { $certDomain } else { $serverIp }
Print-Step "Generating Enterprise certificate..."
Print-Info "SANs: $sanList"
$openssl = Get-Command openssl -ErrorAction SilentlyContinue
if ($openssl) {
$sanArg = "subjectAltName=$sanList"
& openssl req -x509 -nodes -days 3650 -newkey rsa:4096 `
-keyout $keyPath -out $certPath `
-subj "/CN=$cn/O=BetterDesk Enterprise/C=PL" `
-addext $sanArg 2>&1 | Out-Null
if (-not (Test-Path $certPath)) {
& openssl req -x509 -nodes -days 3650 -newkey rsa:4096 `
-keyout $keyPath -out $certPath `
-subj "/CN=$cn/O=BetterDesk Enterprise/C=PL" 2>&1 | Out-Null
}
} else {
$dnsNames = @("localhost")
if ($certDomain) { $dnsNames += $certDomain }
$cert = New-SelfSignedCertificate -DnsName $dnsNames `
-CertStoreLocation "cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(10) `
-KeyExportPolicy Exportable `
-KeyLength 4096
$pfxPath = Join-Path $sslDir "betterdesk.pfx"
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
[System.IO.File]::WriteAllBytes($pfxPath, $certBytes)
$certPath = $pfxPath
$keyPath = $pfxPath
Print-Warning "Generated PFX certificate. Install OpenSSL for PEM format."
}
# === Configure Node.js Console for HTTPS ===
$envContent = $envContent -replace 'HTTPS_ENABLED=.*', 'HTTPS_ENABLED=true'
$envContent = $envContent -replace 'SSL_CERT_PATH=.*', "SSL_CERT_PATH=$certPath"
$envContent = $envContent -replace 'SSL_KEY_PATH=.*', "SSL_KEY_PATH=$keyPath"
$envContent = $envContent -replace 'HTTP_REDIRECT_HTTPS=.*', 'HTTP_REDIRECT_HTTPS=true'
if ($envContent -match 'RUSTDESK_API_TLS=') {
$envContent = $envContent -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=true'
} else {
$envContent = $envContent.TrimEnd() + "`nRUSTDESK_API_TLS=true`n"
}
# Set ALLOW_SELF_SIGNED_CERTS
if ($envContent -match 'ALLOW_SELF_SIGNED_CERTS=') {
$envContent = $envContent -replace 'ALLOW_SELF_SIGNED_CERTS=.*', 'ALLOW_SELF_SIGNED_CERTS=true'
} else {
$envContent = $envContent.TrimEnd() + "`nALLOW_SELF_SIGNED_CERTS=true`n"
}
# Configure NODE_EXTRA_CA_CERTS
if ($envContent -match 'NODE_EXTRA_CA_CERTS=') {
$envContent = $envContent -replace 'NODE_EXTRA_CA_CERTS=.*', "NODE_EXTRA_CA_CERTS=$certPath"
} else {
$envContent = $envContent.TrimEnd() + "`nNODE_EXTRA_CA_CERTS=$certPath`n"
}
# Keep internal Go API URLs on HTTP for RustDesk client compatibility
$envContent = $envContent -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$envContent = $envContent -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
# Set ENTERPRISE_TLS marker
if ($envContent -match 'ENTERPRISE_TLS=') {
$envContent = $envContent -replace 'ENTERPRISE_TLS=.*', 'ENTERPRISE_TLS=true'
} else {
$envContent = $envContent.TrimEnd() + "`nENTERPRISE_TLS=true`n"
}
Set-Content $envFile -Value $envContent -NoNewline
# === Configure Go server with TLS for signal + relay only ===
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
# Remove old TLS args
$goArgs = $goArgs -replace ' -tls-cert [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-key [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-signal', ''
$goArgs = $goArgs -replace ' -tls-relay', ''
$goArgs = $goArgs -replace ' -tls-api', ''
$goArgs = $goArgs -replace ' -force-https', ''
# Add TLS args without -tls-api
$goArgs = "$goArgs -tls-cert $certPath -tls-key $keyPath -tls-signal -tls-relay"
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
Print-Success "Enterprise TLS configured successfully!"
Write-Host ""
Print-Info "Certificate: $certPath"
Print-Info "Valid: 10 years (RSA 4096-bit)"
if ($lanIp -and $lanIp -ne $serverIp) {
Print-Info "LAN IP: $lanIp"
}
Write-Host ""
Write-Host " TLS configured for external channels:" -ForegroundColor Yellow
Print-Info " - Panel HTTPS: :5443 (or configured port)"
Print-Info " - Signal TLS: :21116"
Print-Info " - Relay TLS: :21117"
Print-Info " - Go API HTTP: :$($script:GO_API_PORT) (compat proxy :$($script:CLIENT_API_PORT))"
Write-Host ""
Print-Warning "For browsers/clients, you may need to import $certPath as trusted CA"
}
default {
Print-Warning "Invalid option"
Press-Enter
return
}
}
# ── Update API URLs in .env when SSL is enabled/disabled ──
# Go API TLS (--tls-api) is intentionally not enabled by SSL options.
# RustDesk desktop clients use plain HTTP on :GO_API_PORT or compat :CLIENT_API_PORT.
$envContent = Get-Content $envFile -Raw
if ($sslChoice -eq "5") {
# === Enterprise TLS compatibility mode: Go API stays HTTP ===
Print-Info "Enterprise TLS mode: panel/signal/relay use TLS; Go API stays HTTP"
$envContent = $envContent -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$envContent = $envContent -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
# Ensure NSSM service has ALLOW_SELF_SIGNED_CERTS
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$svcName = $script:CONSOLE_SERVICE
try {
$currentEnv = & nssm get $svcName AppEnvironmentExtra 2>$null
if ($currentEnv) {
if ($currentEnv -notmatch 'ALLOW_SELF_SIGNED_CERTS=') {
$currentEnv = "$currentEnv`nALLOW_SELF_SIGNED_CERTS=true"
}
if ($currentEnv -match 'RUSTDESK_API_TLS=') {
$currentEnv = $currentEnv -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=true'
} else {
$currentEnv = "$currentEnv`nRUSTDESK_API_TLS=true"
}
$currentEnv = $currentEnv -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$currentEnv = $currentEnv -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
& nssm set $svcName AppEnvironmentExtra $currentEnv 2>$null
}
} catch { }
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
$goArgs = $goArgs -replace ' -tls-api', ''
$goArgs = $goArgs -replace ' -force-https', ''
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
} elseif ($sslChoice -ne "4") {
# === Standard SSL (options 1-3): API stays HTTP for RustDesk client compatibility ===
$envContent = $envContent -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$envContent = $envContent -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
# For self-signed certs, Node.js needs NODE_EXTRA_CA_CERTS to trust the CA
$sslCertValue = [regex]::Match($envContent, 'SSL_CERT_PATH=(.+)').Groups[1].Value.Trim()
if ($sslCertValue -and (Test-Path $sslCertValue -ErrorAction SilentlyContinue)) {
if ($envContent -match 'NODE_EXTRA_CA_CERTS=') {
$envContent = $envContent -replace 'NODE_EXTRA_CA_CERTS=.*', "NODE_EXTRA_CA_CERTS=$sslCertValue"
} else {
$envContent = $envContent.TrimEnd() + "`nNODE_EXTRA_CA_CERTS=$sslCertValue`n"
}
Print-Info "NODE_EXTRA_CA_CERTS set to $sslCertValue"
}
# Also update NSSM service environment if available
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$svcName = $script:CONSOLE_SERVICE
try {
$currentEnv = & nssm get $svcName AppEnvironmentExtra 2>$null
if ($currentEnv) {
# Ensure API URLs stay HTTP in NSSM service
$currentEnv = $currentEnv -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$currentEnv = $currentEnv -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
$apiTlsMode = if ($sslChoice -eq "3") { 'false' } else { 'true' }
if ($currentEnv -match 'RUSTDESK_API_TLS=') {
$currentEnv = $currentEnv -replace 'RUSTDESK_API_TLS=.*', "RUSTDESK_API_TLS=$apiTlsMode"
} else {
$currentEnv = "$currentEnv`nRUSTDESK_API_TLS=$apiTlsMode"
}
& nssm set $svcName AppEnvironmentExtra $currentEnv 2>$null
}
} catch { }
# Remove -tls-api from Go server service (standard SSL doesn't use API TLS)
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
$goArgs = $goArgs -replace ' -tls-api', ''
$goArgs = $goArgs -replace ' -force-https', ''
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
Print-Info "Signal/relay TLS enabled, API stays HTTP (RustDesk client compatibility)"
} else {
# === SSL disabled (option 4) — revert API URLs to HTTP ===
$envContent = $envContent -replace 'HBBS_API_URL=https://localhost', 'HBBS_API_URL=http://localhost'
$envContent = $envContent -replace 'BETTERDESK_API_URL=https://localhost', 'BETTERDESK_API_URL=http://localhost'
$envContent = $envContent -replace '(?m)^NODE_EXTRA_CA_CERTS=.*\r?\n?', ''
$envContent = $envContent -replace '(?m)^ENTERPRISE_TLS=.*\r?\n?', ''
$envContent = $envContent -replace 'ALLOW_SELF_SIGNED_CERTS=.*', 'ALLOW_SELF_SIGNED_CERTS=false'
# Remove ALL TLS args from Go server service
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
if ($nssm) {
$svcName = $script:CONSOLE_SERVICE
try {
$currentEnv = & nssm get $svcName AppEnvironmentExtra 2>$null
if ($currentEnv) {
if ($currentEnv -match 'RUSTDESK_API_TLS=') {
$currentEnv = $currentEnv -replace 'RUSTDESK_API_TLS=.*', 'RUSTDESK_API_TLS=false'
} else {
$currentEnv = "$currentEnv`nRUSTDESK_API_TLS=false"
}
& nssm set $svcName AppEnvironmentExtra $currentEnv 2>$null
}
} catch { }
$goSvcName = $script:SERVER_SERVICE
try {
$goArgs = & nssm get $goSvcName AppParameters 2>$null
if ($goArgs) {
$goArgs = $goArgs -replace ' -tls-cert [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-key [^ ]*', ''
$goArgs = $goArgs -replace ' -tls-signal', ''
$goArgs = $goArgs -replace ' -tls-relay', ''
$goArgs = $goArgs -replace ' -tls-api', ''
$goArgs = $goArgs -replace ' -force-https', ''
& nssm set $goSvcName AppParameters $goArgs 2>$null
}
} catch { }
}
Print-Info "All TLS disabled, API URLs reverted to HTTP"
}
Set-Content $envFile -Value $envContent -NoNewline
Write-Host ""
if (Confirm-Action "Restart BetterDesk to apply changes?") {
$serverService = $script:SERVER_SERVICE
$consoleService = $script:CONSOLE_SERVICE
if (Get-Service -Name $serverService -ErrorAction SilentlyContinue) {
Restart-Service -Name $serverService -Force -ErrorAction SilentlyContinue
}
if (Get-Service -Name $consoleService -ErrorAction SilentlyContinue) {
Restart-Service -Name $consoleService -Force -ErrorAction SilentlyContinue
}
Print-Success "BetterDesk services restarted"
}
Press-Enter
}
#===============================================================================
# HTTP/HTTPS Protocol Toggle
#===============================================================================
function Do-ToggleProtocol {
Print-Header
Write-Host "========== PROTOCOL TOGGLE (HTTP / HTTPS) ==========" -ForegroundColor White
Write-Host ""
$envFile = Join-Path $script:CONSOLE_PATH ".env"
$sslDir = Join-Path $script:RUSTDESK_PATH "ssl"
# Detect current mode from NSSM or .env
$currentMode = "HTTP"
$nssmConsole = "BetterDeskConsole"
try {
$nssmEnv = (nssm get $nssmConsole AppEnvironmentExtra 2>$null) -join "`n"
if ($nssmEnv -match "HTTPS_ENABLED=true") {
$currentMode = "HTTPS"
}
} catch {
if (Test-Path $envFile) {
$envContent = Get-Content $envFile -Raw
if ($envContent -match "HTTPS_ENABLED=true") {
$currentMode = "HTTPS"
}
}
}
$tlsSignal = "no"
$tlsRelay = "no"
$nssmServer = "BetterDeskServer"
try {
$serverArgs = nssm get $nssmServer AppParameters 2>$null
if ($serverArgs -match "-tls-signal") { $tlsSignal = "yes" }
if ($serverArgs -match "-tls-relay") { $tlsRelay = "yes" }
} catch {}
Write-Host " Current mode: $currentMode" -ForegroundColor White
Write-Host " Signal TLS: $tlsSignal"
Write-Host " Relay TLS: $tlsRelay"
Write-Host ""
$items = @(
"Switch to HTTP`tEverything plain - LAN / testing",
"Switch to HTTPS`tPanel HTTPS + signal/relay TLS",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "0")
Invoke-MenuChoose -Title "Protocol Mode" -Subtitle "Current mode: $currentMode" -Items $items -Returns $returns
$protoChoice = $script:MENU_CHOICE
switch ($protoChoice) {
"1" {
# --- Switch to HTTP ---
Write-Host ""
Print-Step "Switching to HTTP mode..."
# Update .env
if (Test-Path $envFile) {
$content = Get-Content $envFile -Raw
$content = $content -replace "(?m)^HTTPS_ENABLED=.*$", "HTTPS_ENABLED=false"
$content = $content -replace "(?m)^RUSTDESK_API_TLS=.*$", "RUSTDESK_API_TLS=false"
$content = $content -replace "(?m)^ALLOW_SELF_SIGNED_CERTS=.*$", "ALLOW_SELF_SIGNED_CERTS=false"
$content = $content -replace "(?m)^HBBS_API_URL=https://localhost", "HBBS_API_URL=http://localhost"
$content = $content -replace "(?m)^BETTERDESK_API_URL=https://localhost", "BETTERDESK_API_URL=http://localhost"
$content = $content -replace "(?m)^HTTP_REDIRECT_HTTPS=.*$", "HTTP_REDIRECT_HTTPS=false"
$content = $content -replace "(?m)^NODE_EXTRA_CA_CERTS=.*`n?", ""
$content = $content -replace "(?m)^ENTERPRISE_TLS=.*`n?", ""
Set-Content -Path $envFile -Value $content.TrimEnd() -Encoding UTF8
}
# Update NSSM console service
try {
$env = (nssm get $nssmConsole AppEnvironmentExtra 2>$null) -join "`n"
$env = $env -replace "HTTPS_ENABLED=true", "HTTPS_ENABLED=false"
$env = $env -replace "ALLOW_SELF_SIGNED_CERTS=true", "ALLOW_SELF_SIGNED_CERTS=false"
$env = $env -replace "RUSTDESK_API_TLS=[^\s]+", "RUSTDESK_API_TLS=false"
$env = $env -replace "HBBS_API_URL=https://localhost", "HBBS_API_URL=http://localhost"
$env = $env -replace "BETTERDESK_API_URL=https://localhost", "BETTERDESK_API_URL=http://localhost"
$env = $env -replace "(?m)^NODE_EXTRA_CA_CERTS=.*$", ""
$env = $env -replace "(?m)^ENTERPRISE_TLS=.*$", ""
$env = ($env -split "`n" | Where-Object { $_.Trim() -ne "" }) -join "`n"
nssm set $nssmConsole AppEnvironmentExtra $env 2>$null | Out-Null
} catch {}
# Remove TLS args from Go server
try {
$args = nssm get $nssmServer AppParameters 2>$null
$args = $args -replace '\s*-tls-cert\s+[^\s]+', ''
$args = $args -replace '\s*-tls-key\s+[^\s]+', ''
$args = $args -replace '\s*-tls-signal', ''
$args = $args -replace '\s*-tls-relay', ''
$args = $args -replace '\s*-tls-api', ''
$args = $args -replace '\s*-force-https', ''
nssm set $nssmServer AppParameters $args.Trim() 2>$null | Out-Null
} catch {}
Print-Success "Switched to HTTP mode"
Write-Host ""
Print-Info " Panel: HTTP :5000"
Print-Info " Signal: TCP :21116"
Print-Info " Relay: TCP :21117"
Print-Info " Go API: HTTP :$($script:GO_API_PORT) (default)"
Print-Info " Client API: HTTP :$($script:CLIENT_API_PORT) (compat proxy)"
Write-Host ""
Print-Warning "SSL certificates were NOT deleted (use option C > 4 to remove)"
}
"2" {
# --- Switch to HTTPS ---
Write-Host ""
$certFile = Join-Path $sslDir "betterdesk.crt"
$keyFile = Join-Path $sslDir "betterdesk.key"
# Check for SSL certificates
if (-not (Test-Path $certFile) -or -not (Test-Path $keyFile)) {
Print-Warning "No SSL certificates found at $sslDir"
Write-Host ""
$gen = Read-Host "Generate self-signed certificate now? [Y/n]"
if ($gen -ne "n" -and $gen -ne "N") {
if (-not (Test-Path $sslDir)) { New-Item -ItemType Directory -Path $sslDir -Force | Out-Null }
$serverIp = try {
(Invoke-WebRequest -Uri "https://api.ipify.org" -TimeoutSec 5 -UseBasicParsing).Content.Trim()
} catch { "127.0.0.1" }
& openssl req -x509 -nodes -days 3650 -newkey rsa:4096 `
-keyout $keyFile -out $certFile `
-subj "/CN=$serverIp/O=BetterDesk/C=PL" 2>$null
if (Test-Path $certFile) {
Print-Success "Self-signed certificate generated"
} else {
Print-Error "Failed to generate certificate (is openssl installed?)"
Press-Enter
return
}
} else {
Print-Error "Cannot enable HTTPS without certificates"
Print-Info "Use option C (SSL config) to set up certificates first"
Press-Enter
return
}
}
Print-Step "Switching to HTTPS mode..."
# Update .env
if (Test-Path $envFile) {
$content = Get-Content $envFile -Raw
$content = $content -replace "(?m)^HTTPS_ENABLED=.*$", "HTTPS_ENABLED=true"
$content = $content -replace "(?m)^SSL_CERT_PATH=.*$", "SSL_CERT_PATH=$certFile"
$content = $content -replace "(?m)^SSL_KEY_PATH=.*$", "SSL_KEY_PATH=$keyFile"
$content = $content -replace "(?m)^HTTP_REDIRECT_HTTPS=.*$", "HTTP_REDIRECT_HTTPS=true"
# Keep Go API on HTTP
$content = $content -replace "(?m)^HBBS_API_URL=https://localhost", "HBBS_API_URL=http://localhost"
$content = $content -replace "(?m)^BETTERDESK_API_URL=https://localhost", "BETTERDESK_API_URL=http://localhost"
if ($content -notmatch "ALLOW_SELF_SIGNED_CERTS=") {
$content += "`nALLOW_SELF_SIGNED_CERTS=true"
} else {
$content = $content -replace "(?m)^ALLOW_SELF_SIGNED_CERTS=.*$", "ALLOW_SELF_SIGNED_CERTS=true"
}
if ($content -notmatch "NODE_EXTRA_CA_CERTS=") {
$content += "`nNODE_EXTRA_CA_CERTS=$certFile"
} else {
$content = $content -replace "(?m)^NODE_EXTRA_CA_CERTS=.*$", "NODE_EXTRA_CA_CERTS=$certFile"
}
Set-Content -Path $envFile -Value $content.TrimEnd() -Encoding UTF8
}
# Update NSSM console service
try {
$env = (nssm get $nssmConsole AppEnvironmentExtra 2>$null) -join "`n"
$env = $env -replace "HTTPS_ENABLED=false", "HTTPS_ENABLED=true"
if ($env -notmatch "HTTPS_ENABLED=") { $env += "`nHTTPS_ENABLED=true" }
$env = $env -replace "ALLOW_SELF_SIGNED_CERTS=false", "ALLOW_SELF_SIGNED_CERTS=true"
if ($env -notmatch "ALLOW_SELF_SIGNED_CERTS=") { $env += "`nALLOW_SELF_SIGNED_CERTS=true" }
# Go API stays HTTP
$env = $env -replace "HBBS_API_URL=https://localhost", "HBBS_API_URL=http://localhost"
$env = $env -replace "BETTERDESK_API_URL=https://localhost", "BETTERDESK_API_URL=http://localhost"
if ($env -notmatch "NODE_EXTRA_CA_CERTS=") { $env += "`nNODE_EXTRA_CA_CERTS=$certFile" }
else { $env = $env -replace "(?m)^NODE_EXTRA_CA_CERTS=.*$", "NODE_EXTRA_CA_CERTS=$certFile" }
$env = ($env -split "`n" | Where-Object { $_.Trim() -ne "" }) -join "`n"
nssm set $nssmConsole AppEnvironmentExtra $env 2>$null | Out-Null
} catch {}
# Add TLS to Go server (signal + relay only, NOT API)
try {
$args = nssm get $nssmServer AppParameters 2>$null
# Remove old TLS args
$args = $args -replace '\s*-tls-cert\s+[^\s]+', ''
$args = $args -replace '\s*-tls-key\s+[^\s]+', ''
$args = $args -replace '\s*-tls-signal', ''
$args = $args -replace '\s*-tls-relay', ''
$args = $args -replace '\s*-tls-api', ''
$args = $args -replace '\s*-force-https', ''
# Add signal + relay TLS (API stays HTTP)
$args = "$($args.Trim()) -tls-cert $certFile -tls-key $keyFile -tls-signal -tls-relay"
nssm set $nssmServer AppParameters $args 2>$null | Out-Null
} catch {}
Print-Success "Switched to HTTPS mode"
Write-Host ""
Print-Info " Panel: HTTPS :5443"
Print-Info " Signal: TLS :21116"
Print-Info " Relay: TLS :21117"
Print-Info " Go API: HTTP :$($script:GO_API_PORT) (default, always HTTP)"
Print-Info " Client API: HTTP :$($script:CLIENT_API_PORT) (compat proxy)"
}
default { return }
}
Write-Host ""
$restart = Read-Host "Restart BetterDesk services now? [Y/n]"
if ($restart -ne "n" -and $restart -ne "N") {
try {
nssm restart $nssmServer 2>$null | Out-Null
nssm restart $nssmConsole 2>$null | Out-Null
Start-Sleep -Seconds 2
Print-Success "BetterDesk services restarted"
Write-Host ""
# Quick status check
$serverStatus = (nssm status $nssmServer 2>$null)
$consoleStatus = (nssm status $nssmConsole 2>$null)
if ($serverStatus -match "Running") { Print-Success "Go Server: running" }
else { Print-Error "Go Server: $serverStatus" }
if ($consoleStatus -match "Running") { Print-Success "Web Console: running" }
else { Print-Error "Web Console: $consoleStatus" }
} catch {
Print-Error "Failed to restart services: $_"
}
}
Press-Enter
}
#===============================================================================
# Database Migration Functions
#===============================================================================
function Do-MigrateDatabase {
Print-Header
Write-Host "========== DATABASE MIGRATION ==========" -ForegroundColor White
Write-Host ""
# Locate migration binary
$migrateBin = $null
$searchPaths = @(
(Join-Path $script:ScriptDir "betterdesk-server\tools\migrate\migrate.exe"),
(Join-Path $script:ScriptDir "tools\migrate\migrate.exe"),
(Join-Path $script:RUSTDESK_PATH "migrate.exe"),
"C:\BetterDesk\migrate.exe"
)
foreach ($p in $searchPaths) {
if (Test-Path $p) {
$migrateBin = $p
break
}
}
if (-not $migrateBin) {
Print-Error "Migration binary not found!"
Print-Info "Expected at: $(Join-Path $script:ScriptDir 'betterdesk-server\tools\migrate\migrate.exe')"
Print-Info "Build it with: cd betterdesk-server; go build -o tools\migrate\migrate.exe ./tools/migrate/"
Press-Enter
return
}
Print-Info "Migration binary: $migrateBin"
Write-Host ""
$items = @(
"Rust -> Go`tLegacy Rust hbbs database to Go server",
"Node.js -> Go`tNode.js web console to Go server",
"SQLite -> PostgreSQL`tBetterDesk Go SQLite to PostgreSQL",
"PostgreSQL -> SQLite`tPostgreSQL back to SQLite",
"Backup`tCreate a timestamped SQLite backup",
"Back`tReturn to the main menu"
)
$returns = @("1", "2", "3", "4", "5", "0")
Invoke-MenuChoose -Title "Database Migration" -Subtitle "Move data between BetterDesk components" -Items $items -Returns $returns
$migChoice = $script:MENU_CHOICE
switch ($migChoice) {
"1" {
# Rust -> Go
Write-Host ""
$defaultSrc = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$srcDb = Read-Host "Source Rust database [$defaultSrc]"
if ([string]::IsNullOrEmpty($srcDb)) { $srcDb = $defaultSrc }
if (-not (Test-Path $srcDb)) {
Print-Error "Source database not found: $srcDb"
Press-Enter
return
}
$dstDb = Read-Host "Destination (SQLite path or postgres:// URI) [new file next to source]"
Print-Step "Creating backup before migration..."
& $migrateBin -mode backup -src $srcDb 2>&1 | ForEach-Object { Write-Host $_ }
Print-Step "Running Rust -> Go migration..."
if ([string]::IsNullOrEmpty($dstDb)) {
& $migrateBin -mode rust2go -src $srcDb 2>&1 | ForEach-Object { Write-Host $_ }
} else {
& $migrateBin -mode rust2go -src $srcDb -dst $dstDb 2>&1 | ForEach-Object { Write-Host $_ }
}
if ($LASTEXITCODE -eq 0) {
Print-Success "Rust -> Go migration completed successfully!"
} else {
Print-Error "Migration failed. Check the output above for details."
}
}
"2" {
# Node.js -> Go
Write-Host ""
$defaultSrc = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$defaultAuth = Join-Path $script:CONSOLE_PATH "data\auth.db"
$srcDb = Read-Host "Source Node.js peer database [$defaultSrc]"
if ([string]::IsNullOrEmpty($srcDb)) { $srcDb = $defaultSrc }
if (-not (Test-Path $srcDb)) {
Print-Error "Source peer database not found: $srcDb"
Press-Enter
return
}
$authDb = Read-Host "Node.js auth database [$defaultAuth]"
if ([string]::IsNullOrEmpty($authDb)) { $authDb = $defaultAuth }
$dstDb = Read-Host "Destination (SQLite path or postgres:// URI) [new file next to source]"
Print-Step "Creating backup before migration..."
& $migrateBin -mode backup -src $srcDb 2>&1 | ForEach-Object { Write-Host $_ }
if (Test-Path $authDb) {
& $migrateBin -mode backup -src $authDb 2>&1 | ForEach-Object { Write-Host $_ }
}
Print-Step "Running Node.js -> Go migration..."
$args = @("-mode", "nodejs2go", "-src", $srcDb)
if (Test-Path $authDb) {
$args += @("-node-auth", $authDb)
}
if (-not [string]::IsNullOrEmpty($dstDb)) {
$args += @("-dst", $dstDb)
}
& $migrateBin @args 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) {
Print-Success "Node.js -> Go migration completed successfully!"
} else {
Print-Error "Migration failed. Check the output above for details."
}
}
"3" {
# SQLite -> PostgreSQL
Write-Host ""
$defaultSrc = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$srcDb = Read-Host "Source SQLite database [$defaultSrc]"
if ([string]::IsNullOrEmpty($srcDb)) { $srcDb = $defaultSrc }
if (-not (Test-Path $srcDb)) {
Print-Error "Source database not found: $srcDb"
Press-Enter
return
}
$pgUri = Read-Host "PostgreSQL connection URI (postgres://user:pass@host:5432/dbname)"
if ([string]::IsNullOrEmpty($pgUri)) {
Print-Error "PostgreSQL URI is required"
Press-Enter
return
}
Print-Step "Creating backup before migration..."
& $migrateBin -mode backup -src $srcDb 2>&1 | ForEach-Object { Write-Host $_ }
Print-Step "Running SQLite -> PostgreSQL migration..."
& $migrateBin -mode sqlite2pg -src $srcDb -dst $pgUri 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) {
Print-Success "SQLite -> PostgreSQL migration completed successfully!"
Print-Info "Update your BetterDesk Go server config: DB_URL=$pgUri"
} else {
Print-Error "Migration failed. Check the output above for details."
}
}
"4" {
# PostgreSQL -> SQLite
Write-Host ""
$pgUri = Read-Host "PostgreSQL connection URI (postgres://user:pass@host:5432/dbname)"
if ([string]::IsNullOrEmpty($pgUri)) {
Print-Error "PostgreSQL URI is required"
Press-Enter
return
}
$defaultDst = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$dstDb = Read-Host "Destination SQLite file [$defaultDst]"
if ([string]::IsNullOrEmpty($dstDb)) { $dstDb = $defaultDst }
if (Test-Path $dstDb) {
Print-Warning "Destination file exists: $dstDb"
if (-not (Confirm-Action "Overwrite (backup will be created first)?")) {
Press-Enter
return
}
& $migrateBin -mode backup -src $dstDb 2>&1 | ForEach-Object { Write-Host $_ }
}
Print-Step "Running PostgreSQL -> SQLite migration..."
& $migrateBin -mode pg2sqlite -src $pgUri -dst $dstDb 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) {
Print-Success "PostgreSQL -> SQLite migration completed successfully!"
} else {
Print-Error "Migration failed. Check the output above for details."
}
}
"5" {
# Backup
Write-Host ""
$defaultSrc = Join-Path $script:RUSTDESK_PATH "db_v2.sqlite3"
$srcDb = Read-Host "SQLite database to backup [$defaultSrc]"
if ([string]::IsNullOrEmpty($srcDb)) { $srcDb = $defaultSrc }
if (-not (Test-Path $srcDb)) {
Print-Error "Database not found: $srcDb"
Press-Enter
return
}
Print-Step "Creating backup..."
& $migrateBin -mode backup -src $srcDb 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) {
Print-Success "Backup created successfully!"
} else {
Print-Error "Backup failed."
}
}
"0" { return }
default {
Print-Warning "Invalid option"
}
}
Press-Enter
}
#===============================================================================
# Main Menu
#===============================================================================
function Show-Menu {
Print-Header
Print-Status
Write-Host "========== MAIN MENU ==========" -ForegroundColor White
Write-Host ""
Write-Host " 1. FRESH INSTALLATION"
Write-Host " 2. UPDATE"
Write-Host " 3. REPAIR INSTALLATION"
Write-Host " 4. INSTALLATION VALIDATION"
Write-Host " 5. Backup"
Write-Host " 6. Reset admin password"
Write-Host " 7. Build & deploy server"
Write-Host " 8. DIAGNOSTICS"
Write-Host " 9. UNINSTALL"
Write-Host ""
Write-Host " L. MINIMAL INSTALLATION (server only)"
Write-Host " C. Configure SSL certificates"
Write-Host " T. Toggle HTTP/HTTPS mode"
Write-Host " M. Database migration"
Write-Host " S. Settings (paths)"
Write-Host " 0. Exit"
Write-Host ""
}
function Main {
# Auto-detect paths on startup
Write-Host "Detecting installation..." -ForegroundColor Cyan
Auto-DetectPaths
Write-Host ""
Start-Sleep -Seconds 1
if ($script:UNINSTALL_MODE -and -not $script:AUTO_MODE) {
Do-Uninstall
exit 0
}
# Auto mode - run installation directly
if ($script:AUTO_MODE) {
Print-Info "Running in AUTO mode..."
if ($script:UNINSTALL_MODE) {
Do-Uninstall
} elseif ($script:MINIMAL_MODE) {
Do-InstallMinimal
} else {
Do-Install
}
exit 0
}
while ($true) {
$menuLabels = @(
"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 mode",
"Database migration`tMigrate between backends",
"Settings (paths)`tConfigure install paths",
"Exit`tQuit the manager"
)
$menuActions = @("1", "2", "3", "4", "5", "6", "7", "8", "9", "L", "C", "T", "M", "S", "0")
$choice = ""
if (Test-TuiAvailable) {
$statusLine = "BetterDesk Console Manager v$VERSION"
if (Invoke-TuiSelect -Title "BetterDesk Console Manager v$VERSION" -Subtitle "Use arrow keys, Enter to select" -Items $menuLabels) {
$choice = $menuActions[$script:TUI_RESULT]
} else {
$choice = "0"
}
} else {
Show-Menu
$choice = Read-Host "Select option"
}
switch ($choice) {
"1" { Do-Install }
"2" { Do-Update }
"3" { Do-Repair }
"4" { Do-Validate }
"5" { Do-Backup }
"6" { Do-ResetPassword }
"7" { Do-Build }
"8" { Do-Diagnostics }
"9" { Do-Uninstall }
"L" { Do-InstallMinimal }
"l" { Do-InstallMinimal }
"C" { Do-ConfigureSSL }
"c" { Do-ConfigureSSL }
"T" { Do-ToggleProtocol }
"t" { Do-ToggleProtocol }
"M" { Do-MigrateDatabase }
"m" { Do-MigrateDatabase }
"S" { Configure-Paths }
"s" { Configure-Paths }
"0" {
Write-Host ""
Print-Info "Goodbye!"
exit 0
}
default {
Print-Warning "Invalid option"
Start-Sleep -Seconds 1
}
}
}
}
# Run
Main