mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
6097 lines
247 KiB
PowerShell
6097 lines
247 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
<#
|
|
.SYNOPSIS
|
|
BetterDesk Console Manager v3.5.92 - 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.92"
|
|
$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
|