mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
648a8d4f8a
Commit web-nodejs lockfile with tar override; CI uses npm ci and moderate npm audit. Add LOG_LEVEL filtering and log redaction in Node console and Go server. Validate WS tokens on bd-signal; require single-use token for remote-agent relay. Limit active relay sessions per IP and block open enrollment without TLS.
269 lines
12 KiB
JavaScript
269 lines
12 KiB
JavaScript
/**
|
|
* BetterDesk Console - Configuration
|
|
* Loads settings from environment variables with sensible defaults
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const crypto = require('crypto');
|
|
|
|
// ===== Load .env file as fallback (for Windows NSSM compatibility) =====
|
|
// On Linux, systemd uses EnvironmentFile to load .env; on Windows (NSSM) there
|
|
// is no such mechanism, so we parse .env manually here. Existing env vars
|
|
// (set by NSSM AppEnvironmentExtra or the OS) are never overridden.
|
|
const _envFile = path.join(__dirname, '..', '.env');
|
|
if (fs.existsSync(_envFile)) {
|
|
try {
|
|
const _lines = fs.readFileSync(_envFile, 'utf8').split(/\r?\n/);
|
|
for (const _line of _lines) {
|
|
const _trimmed = _line.trim();
|
|
if (!_trimmed || _trimmed.startsWith('#')) continue;
|
|
const _eq = _trimmed.indexOf('=');
|
|
if (_eq > 0) {
|
|
const _key = _trimmed.substring(0, _eq).trim();
|
|
const _val = _trimmed.substring(_eq + 1).trim();
|
|
if (!process.env[_key]) {
|
|
process.env[_key] = _val;
|
|
}
|
|
}
|
|
}
|
|
} catch (_e) { /* .env read failed — continue with existing env vars */ }
|
|
}
|
|
|
|
const { readProductVersion } = require('../lib/productVersion');
|
|
const pkgVersion = readProductVersion({ consoleDir: path.join(__dirname, '..') });
|
|
|
|
// Environment / platform detection
|
|
const NODE_ENV = process.env.NODE_ENV || 'development';
|
|
const isProduction = NODE_ENV === 'production';
|
|
const isDocker = fs.existsSync('/.dockerenv') || process.env.DOCKER === 'true';
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
// Base paths
|
|
// Support multiple env var names for compatibility with different install scripts
|
|
const DATA_DIR = process.env.DATA_DIR || (isDocker ? '/app/data' : path.join(__dirname, '..', 'data'));
|
|
|
|
// KEYS_PATH: directory where Go server writes .api_key, id_ed25519, id_ed25519.pub
|
|
// Priority: env vars → auto-detect existing directory → platform default
|
|
function resolveKeysPath() {
|
|
const fromEnv = process.env.KEYS_PATH || process.env.RUSTDESK_DIR || process.env.RUSTDESK_PATH;
|
|
if (fromEnv) return fromEnv;
|
|
if (isDocker) return '/opt/rustdesk';
|
|
if (isWindows) {
|
|
// Prefer C:\BetterDesk, fall back to C:\RustDesk for legacy installs
|
|
if (fs.existsSync('C:\\BetterDesk\\id_ed25519')) return 'C:\\BetterDesk';
|
|
if (fs.existsSync('C:\\RustDesk\\id_ed25519')) return 'C:\\RustDesk';
|
|
return 'C:\\BetterDesk';
|
|
}
|
|
// Linux: check both paths, prefer /opt/betterdesk (new), fall back to /opt/rustdesk (legacy)
|
|
if (fs.existsSync('/opt/betterdesk/id_ed25519')) return '/opt/betterdesk';
|
|
if (fs.existsSync('/opt/rustdesk/id_ed25519')) return '/opt/rustdesk';
|
|
// Neither exists yet — use new default
|
|
return '/opt/betterdesk';
|
|
}
|
|
const KEYS_PATH = resolveKeysPath();
|
|
const RUSTDESK_DIR = KEYS_PATH;
|
|
|
|
// Warn if KEYS_PATH was auto-detected and looks wrong
|
|
if (!process.env.KEYS_PATH && !process.env.RUSTDESK_DIR && !process.env.RUSTDESK_PATH) {
|
|
const apiKeyFile = path.join(KEYS_PATH, '.api_key');
|
|
const keyFile = path.join(KEYS_PATH, 'id_ed25519');
|
|
if (!fs.existsSync(apiKeyFile) && !fs.existsSync(keyFile)) {
|
|
console.warn(`⚠️ KEYS_PATH auto-detected as "${KEYS_PATH}" but no .api_key or id_ed25519 found there.`);
|
|
console.warn(' Set KEYS_PATH in .env to point to the Go server data directory.');
|
|
}
|
|
}
|
|
|
|
// Database path
|
|
const DB_PATH = process.env.DB_PATH || path.join(RUSTDESK_DIR, 'db_v2.sqlite3');
|
|
|
|
// Key paths
|
|
const PUB_KEY_PATH = process.env.PUB_KEY_PATH || path.join(KEYS_PATH, 'id_ed25519.pub');
|
|
const API_KEY_PATH = process.env.API_KEY_PATH || path.join(KEYS_PATH, '.api_key');
|
|
|
|
// Read API key from file if exists
|
|
let apiKey = process.env.BETTERDESK_API_KEY || process.env.HBBS_API_KEY || '';
|
|
if (!apiKey && fs.existsSync(API_KEY_PATH)) {
|
|
try {
|
|
apiKey = fs.readFileSync(API_KEY_PATH, 'utf8').trim();
|
|
} catch (err) {
|
|
console.warn('Warning: Could not read API key file:', err.message);
|
|
}
|
|
}
|
|
|
|
// Session secret - generate if not provided
|
|
let sessionSecret = process.env.SESSION_SECRET;
|
|
if (!sessionSecret) {
|
|
const secretFile = path.join(DATA_DIR, '.session_secret');
|
|
if (fs.existsSync(secretFile)) {
|
|
sessionSecret = fs.readFileSync(secretFile, 'utf8').trim();
|
|
} else {
|
|
sessionSecret = crypto.randomBytes(32).toString('hex');
|
|
try {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
fs.writeFileSync(secretFile, sessionSecret, { mode: 0o600 });
|
|
} catch (err) {
|
|
console.warn('Warning: Could not save session secret:', err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go API default port (signal_port - 2). :21121 is optional backward-compat proxy only.
|
|
const GO_API_PORT_DEFAULT = 21114;
|
|
const CLIENT_API_PORT_DEFAULT = 21121;
|
|
|
|
function parseApiUrlPort(urlString, fallback) {
|
|
try {
|
|
const u = new URL(urlString);
|
|
if (u.port) return parseInt(u.port, 10);
|
|
return u.protocol === 'https:' ? 443 : 80;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
const _betterdeskApiUrlRaw = process.env.BETTERDESK_API_URL || process.env.HBBS_API_URL
|
|
|| `http://127.0.0.1:${GO_API_PORT_DEFAULT}/api`;
|
|
const goApiPort = parseInt(process.env.GO_API_PORT, 10)
|
|
|| parseApiUrlPort(_betterdeskApiUrlRaw, GO_API_PORT_DEFAULT);
|
|
|
|
module.exports = {
|
|
// Environment
|
|
nodeEnv: NODE_ENV,
|
|
isProduction,
|
|
isDocker,
|
|
|
|
// Server
|
|
port: parseInt(process.env.PORT, 10) || 5000,
|
|
host: process.env.HOST || '127.0.0.1',
|
|
|
|
// Go server HTTP API (REST + RustDesk handlers) — default :21114
|
|
goApiPort,
|
|
|
|
// Compatibility listener :21121 — reverse-proxy to Go (no business logic in Node).
|
|
// Keeps existing RustDesk client configs on http://host:21121 unchanged.
|
|
apiPort: parseInt(process.env.API_PORT, 10) || CLIENT_API_PORT_DEFAULT,
|
|
apiHost: process.env.API_HOST || '0.0.0.0',
|
|
apiEnabled: (process.env.API_ENABLED ?? 'true').toLowerCase() === 'true',
|
|
apiProxyToGo: (process.env.RUSTDESK_API_PROXY ?? 'true').toLowerCase() === 'true',
|
|
rustdeskApiTls: (process.env.RUSTDESK_API_TLS || 'auto').toLowerCase(),
|
|
|
|
// Issue #104 mitigation:
|
|
// Stock RustDesk OSS clients (e.g. v1.4.6) do not implement the
|
|
// `tfa_check` 2FA challenge response shape returned by this API when a
|
|
// user has TOTP enabled — they reject it as "bad response from server".
|
|
// Setting RUSTDESK_API_DISABLE_TOTP=true makes /api/login on the
|
|
// dedicated client API port (:21121) skip TOTP enforcement and issue an
|
|
// access token directly after password auth. The web panel (port 5000 /
|
|
// HTTPS 5443) is unaffected — TOTP is still enforced there.
|
|
// Trade-off: any caller of /api/login with a valid username+password
|
|
// will obtain a token without 2FA. Mitigations:
|
|
// - keep :21121 firewalled to LAN/VPN where possible,
|
|
// - use a dedicated low-privilege RustDesk service account,
|
|
// - rely on device bans / API key for admin endpoints.
|
|
rustdeskApiDisableTotp: (process.env.RUSTDESK_API_DISABLE_TOTP || 'false').toLowerCase() === 'true',
|
|
|
|
// H-04 mitigation (audit 2026-04-10):
|
|
// Setting RUSTDESK_API_DISABLE_TOTP=true alone is no longer sufficient —
|
|
// the operator must also opt in explicitly with
|
|
// RUSTDESK_API_DISABLE_TOTP_ACKNOWLEDGED=true, confirming they have read
|
|
// and accepted the WAN-port 2FA bypass risk. Without the ACK flag the
|
|
// bypass is ignored and TOTP is enforced normally on :21121.
|
|
rustdeskApiDisableTotpAck: (process.env.RUSTDESK_API_DISABLE_TOTP_ACKNOWLEDGED || 'false').toLowerCase() === 'true',
|
|
|
|
// Device visibility for non-admin roles: open (legacy overlay ACL) or restricted (default-deny).
|
|
deviceScopeDefault: (process.env.DEVICE_SCOPE_DEFAULT || 'open').toLowerCase(),
|
|
|
|
// HTTPS / SSL
|
|
httpsEnabled: (process.env.HTTPS_ENABLED || 'false').toLowerCase() === 'true',
|
|
httpsPort: parseInt(process.env.HTTPS_PORT, 10) || 5443,
|
|
sslCertPath: process.env.SSL_CERT_PATH || '',
|
|
sslKeyPath: process.env.SSL_KEY_PATH || '',
|
|
sslCaPath: process.env.SSL_CA_PATH || '',
|
|
httpRedirect: (process.env.HTTP_REDIRECT_HTTPS || 'true').toLowerCase() === 'true',
|
|
|
|
// Paths
|
|
dataDir: DATA_DIR,
|
|
keysPath: KEYS_PATH,
|
|
rustdeskDir: RUSTDESK_DIR,
|
|
dbPath: DB_PATH,
|
|
pubKeyPath: PUB_KEY_PATH,
|
|
apiKeyPath: API_KEY_PATH,
|
|
|
|
// Server backend (BetterDesk Go server)
|
|
serverBackend: 'betterdesk',
|
|
|
|
// BetterDesk Go Server API
|
|
hbbsApiUrl: _betterdeskApiUrlRaw,
|
|
hbbsApiKey: apiKey,
|
|
hbbsApiTimeout: parseInt(process.env.BETTERDESK_API_TIMEOUT || process.env.HBBS_API_TIMEOUT, 10) || 3000,
|
|
|
|
// BetterDesk Go Server API (preferred names)
|
|
betterdeskApiUrl: _betterdeskApiUrlRaw,
|
|
betterdeskApiKey: process.env.BETTERDESK_API_KEY || apiKey,
|
|
betterdeskApiTimeout: parseInt(process.env.BETTERDESK_API_TIMEOUT, 10) || 5000,
|
|
|
|
// TLS certificate verification (BD-2026-002)
|
|
// Default is false (reject self-signed certs) for production safety.
|
|
// Set ALLOW_SELF_SIGNED_CERTS=true only in dev/local environments where
|
|
// the Go API is accessed over HTTPS with a self-signed cert.
|
|
allowSelfSignedCerts: (process.env.ALLOW_SELF_SIGNED_CERTS || 'false').toLowerCase() === 'true',
|
|
// SMTP TLS verification — separate control for outbound email.
|
|
// Set to 'true' when using a trusted SMTP server with valid certificates.
|
|
smtpTlsVerify: (process.env.SMTP_TLS_VERIFY || 'false').toLowerCase() === 'true',
|
|
|
|
// Session
|
|
sessionSecret: sessionSecret,
|
|
sessionMaxAge: parseInt(process.env.SESSION_MAX_AGE, 10) || 24 * 60 * 60 * 1000, // 24 hours
|
|
|
|
// Rate limiting
|
|
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60 * 1000, // 1 minute
|
|
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
|
|
loginRateLimitMax: parseInt(process.env.LOGIN_RATE_LIMIT_MAX, 10) || 5,
|
|
|
|
// i18n
|
|
defaultLanguage: process.env.DEFAULT_LANGUAGE || 'en',
|
|
langDir: path.join(__dirname, '..', 'lang'),
|
|
|
|
// WebSocket Proxy (for remote desktop web client)
|
|
wsProxy: {
|
|
hbbsHost: process.env.WS_HBBS_HOST || 'localhost',
|
|
hbbsPort: parseInt(process.env.WS_HBBS_PORT, 10) || 21116,
|
|
hbbrHost: process.env.WS_HBBR_HOST || 'localhost',
|
|
hbbrPort: parseInt(process.env.WS_HBBR_PORT, 10) || 21117
|
|
},
|
|
|
|
// Database type: 'sqlite' (default) or 'postgres' (auto-detected from DATABASE_URL)
|
|
dbType: (() => {
|
|
const explicit = (process.env.DB_TYPE || '').toLowerCase();
|
|
if (explicit === 'postgres' || explicit === 'postgresql') return 'postgres';
|
|
if (explicit === 'sqlite') return 'sqlite';
|
|
if (!explicit && process.env.DATABASE_URL && /^postgres(ql)?:\/\//i.test(process.env.DATABASE_URL)) return 'postgres';
|
|
return 'sqlite';
|
|
})(),
|
|
databaseUrl: process.env.DATABASE_URL || '',
|
|
|
|
// App info
|
|
appName: 'BetterDesk Console',
|
|
appVersion: pkgVersion,
|
|
|
|
// Logging (default warn in production — see lib/logger.js)
|
|
logLevel: (process.env.LOG_LEVEL || '').trim().toLowerCase() || (isProduction ? 'warn' : 'info'),
|
|
};
|
|
|
|
// H-2: warn when console→Go API traffic leaves localhost in production.
|
|
if (isProduction) {
|
|
try {
|
|
const raw = module.exports.betterdeskApiUrl || '';
|
|
const apiUrl = new URL(raw.endsWith('/') ? raw : `${raw}/`);
|
|
const host = apiUrl.hostname.toLowerCase();
|
|
const localHosts = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
const isDockerBridge = host === 'betterdesk-server' || host.endsWith('.betterdesk-net');
|
|
if (!localHosts.has(host) && !isDockerBridge) {
|
|
console.warn(
|
|
`[SECURITY] BETTERDESK_API_URL host is "${host}" — console→Go traffic should stay on localhost or the Docker internal network.`
|
|
);
|
|
}
|
|
} catch (_) { /* invalid URL — other startup checks will surface it */ }
|
|
}
|