Files
BetterDesk/web-nodejs/server.js
T
UNITRONIX 00451634c7 Await DB operations in route handlers
Convert many Express route handlers and helper functions to async and await database calls (e.g. getAccessToken, touchAccessToken, getPeerById, getDevice, getPeerSysinfo, upsertPeerSysinfo, logAction, insertAudit*, getAll*/count* etc.). Also made identifyDevice and several route callbacks async, adjusted session.regenerate callback to use async logging, and replaced db.getDatabase() usage with db.getDb() where applicable. These changes ensure DB operations complete before responding and reduce race conditions/unhandled-promise behavior across numerous route files (activity, auth, automation, bd-api, devices, folders, i18n, inventory, registration, remote, rustdesk-api, and related route handlers).

Co-Authored-By: Charles Olivier Savignac <1275666+sircharlo@users.noreply.github.com>
2026-03-03 22:57:10 +01:00

492 lines
18 KiB
JavaScript

/**
* BetterDesk Console - Server Entry Point
* Professional Web Management Panel for RustDesk Server
*
* @author UNITRONIX
* @version 2.1.0
* @license AGPL-3.0
*/
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
const http = require('http');
const https = require('https');
const config = require('./config/config');
const securityMiddleware = require('./middleware/security');
const { initI18n } = require('./middleware/i18n');
const { apiLimiter } = require('./middleware/rateLimiter');
const { csrfTokenProvider, doubleCsrfProtection } = require('./middleware/csrf');
const authService = require('./services/authService');
const hbbsApi = require('./services/hbbsApi');
const serverBackend = require('./services/serverBackend');
const db = require('./services/database');
const { initWsProxy } = require('./services/wsRelay');
const { initBdRelay } = require('./services/bdRelay');
const { initChatRelay } = require('./services/chatRelay');
const { initRemoteRelay } = require('./services/remoteRelay');
const { startDiscoveryService } = require('./services/lanDiscovery');
const routes = require('./routes');
const rustdeskApiRoutes = require('./routes/rustdesk-api.routes');
const { getWanMiddlewareStack } = require('./middleware/wanSecurity');
// Create Express app
const app = express();
// Trust proxy (for rate limiting behind reverse proxy)
// Configurable via TRUST_PROXY env var: 0=disabled, 1=single proxy, 'loopback'=localhost only
// Default: false (safest). Set TRUST_PROXY=1 when behind nginx/Apache/cloudflare
const trustProxy = process.env.TRUST_PROXY !== undefined ?
(isNaN(process.env.TRUST_PROXY) ? process.env.TRUST_PROXY : parseInt(process.env.TRUST_PROXY, 10)) : false;
app.set('trust proxy', trustProxy);
// View engine setup
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Ensure data directory exists
if (!fs.existsSync(config.dataDir)) {
fs.mkdirSync(config.dataDir, { recursive: true });
}
// ============ Middleware Pipeline ============
// Security headers (Helmet)
app.use(securityMiddleware);
// Body parsing (2MB limit for base64 logo images)
app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: false, limit: '2mb' }));
// Cookie parsing
app.use(cookieParser());
// Session management — also kept as a standalone middleware ref for WebSocket upgrades
const sessionMiddleware = session({
secret: config.sessionSecret,
name: 'betterdesk.sid',
resave: false,
saveUninitialized: false,
cookie: {
secure: config.httpsEnabled,
httpOnly: true,
sameSite: 'lax',
maxAge: config.sessionMaxAge
}
});
app.use(sessionMiddleware);
// Static files
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: config.isProduction ? '1d' : '0',
etag: true
}));
// Serve proto files for remote client (protobufjs dynamic loading)
app.use('/protos', express.static(path.join(__dirname, 'protos'), {
maxAge: config.isProduction ? '7d' : '0',
etag: true
}));
// Rate limiting for API
app.use('/api/', apiLimiter);
// i18n middleware
app.use(initI18n());
// CSRF protection — generate token for views, validate on POST/PUT/DELETE/PATCH
app.use(csrfTokenProvider);
app.use(doubleCsrfProtection);
// ============ Routes ============
app.use('/', routes);
// ============ Error Handlers ============
// CSRF token mismatch
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN' || err.message?.includes('csrf') || err.message?.includes('CSRF')) {
res.status(403);
// Always return JSON for API routes (fetch sends Accept: */*)
if (req.path.startsWith('/api/') || (req.headers['content-type'] && req.headers['content-type'].includes('application/json'))) {
return res.json({ success: false, error: 'Invalid CSRF token. Please refresh the page and try again.' });
}
if (req.accepts('html')) {
return res.render('errors/500', {
title: 'Forbidden',
activePage: 'error',
error: 'Invalid or missing CSRF token. Please refresh the page and try again.'
});
}
return res.json({ success: false, error: 'Invalid CSRF token' });
}
next(err);
});
// 404 Not Found
app.use((req, res, next) => {
res.status(404);
if (req.accepts('html')) {
res.render('errors/404', {
title: req.t('errors.not_found'),
activePage: 'error'
});
} else {
res.json({
success: false,
error: 'Not Found'
});
}
});
// 500 Server Error
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(err.status || 500);
// Always return JSON for API routes
if (req.path.startsWith('/api/') || (req.headers['content-type'] && req.headers['content-type'].includes('application/json'))) {
return res.json({
success: false,
error: config.isProduction ? 'Internal Server Error' : err.message
});
}
if (req.accepts('html')) {
res.render('errors/500', {
title: req.t('errors.server_error'),
activePage: 'error',
error: config.isProduction ? null : err.message
});
} else {
res.json({
success: false,
error: config.isProduction ? 'Internal Server Error' : err.message
});
}
});
// ============ Startup ============
/**
* Load SSL certificates for HTTPS
*/
function loadSslCertificates() {
const options = {};
if (!config.sslCertPath || !config.sslKeyPath) {
return null;
}
try {
if (!fs.existsSync(config.sslCertPath)) {
console.error(`SSL certificate not found: ${config.sslCertPath}`);
return null;
}
if (!fs.existsSync(config.sslKeyPath)) {
console.error(`SSL private key not found: ${config.sslKeyPath}`);
return null;
}
options.cert = fs.readFileSync(config.sslCertPath);
options.key = fs.readFileSync(config.sslKeyPath);
// Optional CA bundle (for Let's Encrypt chain)
if (config.sslCaPath && fs.existsSync(config.sslCaPath)) {
options.ca = fs.readFileSync(config.sslCaPath);
}
return options;
} catch (err) {
console.error('Failed to load SSL certificates:', err.message);
return null;
}
}
/**
* Create HTTP redirect server (redirects all HTTP to HTTPS)
*/
function createHttpRedirectServer() {
const redirectApp = express();
redirectApp.use((req, res) => {
const httpsUrl = `https://${req.hostname}:${config.httpsPort}${req.url}`;
res.redirect(301, httpsUrl);
});
return http.createServer(redirectApp);
}
async function startServer() {
try {
// Initialize database adapter (creates tables, runs migrations)
await db.init();
// Ensure default admin exists
await authService.ensureDefaultAdmin();
let server;
let protocol = 'http';
let displayPort = config.port;
// HTTPS mode
if (config.httpsEnabled) {
const sslOptions = loadSslCertificates();
if (sslOptions) {
// Create HTTPS server
server = https.createServer(sslOptions, app);
protocol = 'https';
displayPort = config.httpsPort;
server.listen(config.httpsPort, config.host, () => {
printStartupBanner(protocol, displayPort);
});
// Optionally start HTTP redirect server
if (config.httpRedirect) {
const redirectServer = createHttpRedirectServer();
redirectServer.listen(config.port, config.host, () => {
console.log(` HTTP -> HTTPS redirect active on port ${config.port}`);
console.log('');
});
// Graceful shutdown for redirect server too
const shutdownRedirect = () => { redirectServer.close(); };
process.on('SIGTERM', shutdownRedirect);
process.on('SIGINT', shutdownRedirect);
}
} else {
console.warn('WARNING: HTTPS enabled but certificates not found/invalid');
console.warn('Falling back to HTTP mode');
server = http.createServer(app);
server.listen(config.port, config.host, () => {
printStartupBanner(protocol, config.port);
});
}
} else {
// HTTP mode (default)
server = http.createServer(app);
server.listen(config.port, config.host, () => {
printStartupBanner(protocol, config.port);
});
}
// Initialize WebSocket proxy for remote desktop client
initWsProxy(server);
// Initialize BetterDesk native relay (WebSocket)
initBdRelay(server);
// Initialize Chat relay (WebSocket — agent ↔ operator)
initChatRelay(server, sessionMiddleware);
// Initialize Remote Desktop relay (WebSocket — agent JPEG ↔ browser viewer)
initRemoteRelay(server, sessionMiddleware);
// Start LAN Discovery UDP service
startDiscoveryService();
// ============ RustDesk Client API Server (dedicated port) ============
let apiServer = null;
if (config.apiEnabled) {
apiServer = startRustDeskApiServer();
}
// ============ Periodic Housekeeping ============
const housekeepingInterval = setInterval(async () => {
await authService.cleanupHousekeeping();
// Clean up old integration data (metrics >7d, audit >90d)
try {
await db.runIntegrationHousekeeping();
} catch (err) {
// Silent fail — don't crash the server for housekeeping
}
}, 60 * 60 * 1000); // Every hour
// ============ Periodic Online Status Sync ============
const syncInterval = parseInt(process.env.STATUS_SYNC_INTERVAL, 10) || 15; // seconds
const heartbeatStaleThreshold = parseInt(process.env.HEARTBEAT_STALE_THRESHOLD, 10) || 90; // seconds
const statusSyncInterval = setInterval(async () => {
try {
await serverBackend.syncOnlineStatus();
} catch (err) {
// Silent fail - don't crash the server
}
// Also clean up stale heartbeat-based online status
try {
if (typeof db.cleanupStaleOnlinePeers === 'function') {
await db.cleanupStaleOnlinePeers(heartbeatStaleThreshold);
}
} catch (err) {
// Silent fail
}
}, syncInterval * 1000);
// Initial sync on startup (after short delay for HBBS to be ready)
setTimeout(async () => {
try {
const result = await serverBackend.syncOnlineStatus();
if (result.synced > 0) {
console.log(`Initial status sync: ${result.synced} device(s) online`);
}
} catch (err) {
// Silent fail
}
}, 5000);
// Graceful shutdown
const shutdown = (signal) => {
console.log(`\n${signal} received. Shutting down gracefully...`);
clearInterval(housekeepingInterval);
clearInterval(statusSyncInterval);
const closePromises = [new Promise(r => server.close(r))];
if (apiServer) {
closePromises.push(new Promise(r => apiServer.close(r)));
}
Promise.all(closePromises).then(() => {
console.log('All servers closed.');
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
/**
* Start the dedicated RustDesk Client API server on a separate port.
* This is a minimal, hardened Express app with only 4 endpoints.
* Designed for WAN/internet exposure with aggressive security.
*/
function startRustDeskApiServer() {
const apiApp = express();
// Trust proxy (use same configuration as main app — TRUST_PROXY env var)
apiApp.set('trust proxy', trustProxy);
// Apply WAN security middleware stack
const wanMiddleware = getWanMiddlewareStack();
for (const mw of wanMiddleware) {
apiApp.use(mw);
}
// JSON body parser with size limit (64KB for address book sync)
apiApp.use(express.json({ limit: '64kb', strict: true }));
// Mount RustDesk-compatible API routes
apiApp.use('/', rustdeskApiRoutes);
// Mount device-facing registration routes (LAN discovery pairing)
const registrationRoutes = require('./routes/registration.routes');
apiApp.use('/api/bd', registrationRoutes);
// Catch-all for any unmatched routes (should not reach here due to pathWhitelist)
apiApp.use((req, res) => {
res.status(404).end();
});
// Error handler — never leak internal errors
apiApp.use((err, req, res, next) => {
if (err.type === 'entity.parse.failed') {
console.warn('RustDesk API: JSON parse error from', req.socket?.remoteAddress);
return res.status(400).json({ error: 'Invalid JSON' });
}
if (err.type === 'entity.too.large') {
return res.status(413).json({ error: 'Request too large' });
}
console.error('RustDesk API error:', err.message);
res.status(500).json({ error: 'Server error' });
});
// Start HTTP or HTTPS server for RustDesk Client API
let apiServerInstance;
if (config.httpsEnabled) {
const sslOptions = loadSslCertificates();
if (sslOptions) {
apiServerInstance = https.createServer(sslOptions, apiApp);
console.log(` ║ API TLS: Enabled (HTTPS)`.padEnd(53) + '║');
} else {
console.warn('WARNING: HTTPS enabled but SSL certs invalid — API falling back to HTTP');
apiServerInstance = http.createServer(apiApp);
}
} else {
apiServerInstance = http.createServer(apiApp);
}
apiServerInstance.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(` ║ API Port: ${config.apiPort} FAILED (port in use)`.padEnd(53) + '║');
console.error(` ║ Hint: Check if hbbs uses the same port, or`.padEnd(53) + '║');
console.error(` ║ set API_PORT env var (default: 21121)`.padEnd(53) + '║');
console.log(' ║ ║');
console.error(`WARNING: RustDesk Client API could not start on port ${config.apiPort}`);
console.error('Likely cause: hbbs API is on the same port. Client API default is 21121.');
console.error('The admin panel continues to run normally on port ' + config.port);
return; // Don't crash — let the panel continue running
}
throw err;
});
apiServerInstance.listen(config.apiPort, config.host, () => {
console.log(` ║ API Port: ${config.apiPort} (RustDesk Client)`.padEnd(53) + '║');
console.log(' ║ ║');
});
// Set connection timeout (prevent slow loris)
apiServerInstance.headersTimeout = 15000;
apiServerInstance.requestTimeout = 10000;
apiServerInstance.keepAliveTimeout = 5000;
return apiServerInstance;
}
/**
* Print startup banner with server info
*/
function printStartupBanner(protocol, port) {
const sslStatus = config.httpsEnabled ? '🔒 HTTPS' : '🔓 HTTP';
const apiStatus = config.apiEnabled ? `✅ Port ${config.apiPort}` : '❌ Disabled';
console.log('');
console.log(' ╔══════════════════════════════════════════════════╗');
console.log(' ║ ║');
console.log(' ║ 🖥️ BetterDesk Console v' + config.appVersion.padEnd(23) + ' ║');
console.log(' ║ ║');
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(' ║ ║');
console.log(` ║ Panel: ${protocol}://${config.host}:${port}`.padEnd(53) + '║');
console.log(` ║ Client API: ${apiStatus}`.padEnd(53) + '║');
console.log(` ║ Mode: ${config.nodeEnv}`.padEnd(53) + '║');
console.log(` ║ Security: ${sslStatus}`.padEnd(53) + '║');
const dbLabel = (db.DB_TYPE === 'postgres' || db.DB_TYPE === 'postgresql')
? `PostgreSQL (${process.env.DATABASE_URL ? new URL(process.env.DATABASE_URL).hostname : 'localhost'})`
: path.basename(config.dbPath);
console.log(` ║ Database: ${dbLabel}`.padEnd(53) + '║');
console.log(' ║ ║');
console.log(' ╚══════════════════════════════════════════════════╝');
console.log('');
}
// Start the server
startServer();
module.exports = app;