mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
f354e83a14
Expose pending LAN and managed enrollment requests in the UX 3.5 notification center, synchronize badges through authenticated panel events with polling fallback, and refresh the registrations view automatically. Also include the pending relay transport fixes already present in the working tree. Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
277 lines
9.5 KiB
JavaScript
277 lines
9.5 KiB
JavaScript
/**
|
|
* BetterDesk Console — Real-time Device Status Push
|
|
*
|
|
* Connects to Go server's WebSocket event bus and pushes device status
|
|
* changes to browser clients in real time.
|
|
*
|
|
* Go server endpoint: GET /api/ws/events
|
|
* Browser endpoints: WS /ws/device-status (device status)
|
|
* WS /ws/panel-events (registration notifications)
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const WebSocket = require('ws');
|
|
const db = require('./database');
|
|
const { roleHasPermission } = require('../middleware/auth');
|
|
|
|
const log = {
|
|
info: (...a) => console.log('[DeviceStatus]', ...a),
|
|
warn: (...a) => console.warn('[DeviceStatus]', ...a),
|
|
error: (...a) => console.error('[DeviceStatus]', ...a),
|
|
};
|
|
|
|
const PING_INTERVAL = 30000;
|
|
const RECONNECT_BASE = 3000;
|
|
const RECONNECT_MAX = 60000;
|
|
|
|
let panelEventBroadcaster = null;
|
|
|
|
/**
|
|
* Publish a local event to authenticated panel-event WebSocket clients.
|
|
* Registration routes use this for events created by Node.js itself (LAN
|
|
* discovery), while Go-originated events use the same browser channel below.
|
|
*/
|
|
function publishPanelEvent(data) {
|
|
if (typeof panelEventBroadcaster === 'function') {
|
|
panelEventBroadcaster(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize real-time device status push.
|
|
* @param {import('http').Server} httpServer - The HTTP server to attach WS to
|
|
* @param {Function} sessionMiddleware - Express session middleware for auth
|
|
* @param {string} goApiUrl - Go server base URL (e.g. http://localhost:21121/api)
|
|
* @param {string} apiKey - API key for Go server authentication
|
|
*/
|
|
function initDeviceStatusPush(httpServer, sessionMiddleware, goApiUrl, apiKey) {
|
|
// Browser-facing WebSocket server
|
|
const wss = new WebSocket.Server({ noServer: true });
|
|
const clients = new Set();
|
|
const panelWss = new WebSocket.Server({ noServer: true });
|
|
const panelClients = new Set();
|
|
const { registerUpgradeHandler } = require('./wsUpgradeRouter');
|
|
|
|
function authenticateUpgrade(req, socket, head, targetWss, authorize) {
|
|
sessionMiddleware(req, {}, () => {
|
|
if (!req.session || !req.session.userId) {
|
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
if (authorize && !authorize(req)) {
|
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
targetWss.handleUpgrade(req, socket, head, (ws) => {
|
|
targetWss.emit('connection', ws, req);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle upgrade requests via shared router (#295)
|
|
registerUpgradeHandler(
|
|
httpServer,
|
|
(pathname) => pathname === '/ws/device-status',
|
|
(req, socket, head) => authenticateUpgrade(req, socket, head, wss)
|
|
);
|
|
registerUpgradeHandler(
|
|
httpServer,
|
|
(pathname) => pathname === '/ws/panel-events',
|
|
(req, socket, head) => authenticateUpgrade(
|
|
req,
|
|
socket,
|
|
head,
|
|
panelWss,
|
|
(request) => roleHasPermission(request.session?.user?.role, 'enrollment.approve')
|
|
)
|
|
);
|
|
|
|
wss.on('connection', (ws) => {
|
|
clients.add(ws);
|
|
|
|
// Ping to keep alive
|
|
const pingTimer = setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
else clearInterval(pingTimer);
|
|
}, PING_INTERVAL);
|
|
|
|
ws.on('close', () => {
|
|
clients.delete(ws);
|
|
clearInterval(pingTimer);
|
|
});
|
|
|
|
ws.on('error', () => {
|
|
clients.delete(ws);
|
|
clearInterval(pingTimer);
|
|
});
|
|
});
|
|
|
|
panelWss.on('connection', (ws) => {
|
|
panelClients.add(ws);
|
|
|
|
const pingTimer = setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
else clearInterval(pingTimer);
|
|
}, PING_INTERVAL);
|
|
|
|
ws.on('close', () => {
|
|
panelClients.delete(ws);
|
|
clearInterval(pingTimer);
|
|
});
|
|
|
|
ws.on('error', () => {
|
|
panelClients.delete(ws);
|
|
clearInterval(pingTimer);
|
|
});
|
|
});
|
|
|
|
// Broadcast to all connected browser clients
|
|
function broadcast(data) {
|
|
const text = JSON.stringify(data);
|
|
for (const ws of clients) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
function broadcastPanelEvent(data) {
|
|
const text = JSON.stringify(data);
|
|
for (const ws of panelClients) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
panelEventBroadcaster = broadcastPanelEvent;
|
|
|
|
// Connect to Go server event bus
|
|
let retryDelay = RECONNECT_BASE;
|
|
|
|
function connectToGoEventBus() {
|
|
const wsUrl = goApiUrl
|
|
.replace(/^http:/, 'ws:')
|
|
.replace(/^https:/, 'wss:')
|
|
.replace(/\/api$/, '');
|
|
|
|
const url = `${wsUrl}/api/ws/events?api_key=${encodeURIComponent(apiKey)}`;
|
|
|
|
log.info('Connecting to Go event bus...');
|
|
|
|
// Only set TLS options when connecting via wss://
|
|
const wsOpts = { headers: { 'X-API-Key': apiKey } };
|
|
if (wsUrl.startsWith('wss://')) {
|
|
wsOpts.rejectUnauthorized = !require('../config/config').allowSelfSignedCerts;
|
|
}
|
|
|
|
const goWs = new WebSocket(url, wsOpts);
|
|
|
|
goWs.on('open', () => {
|
|
log.info('Connected to Go event bus');
|
|
retryDelay = RECONNECT_BASE;
|
|
});
|
|
|
|
goWs.on('message', (data) => {
|
|
try {
|
|
const event = JSON.parse(data.toString());
|
|
const payload = event.data || {};
|
|
|
|
if (event.type === 'peer_id_changed') {
|
|
const oldId = payload.old_id;
|
|
const newId = payload.new_id;
|
|
if (oldId && newId && typeof db.cascadePeerIdChange === 'function') {
|
|
Promise.resolve(db.cascadePeerIdChange(oldId, newId)).catch((err) => {
|
|
log.warn('Panel ID cascade failed:', err.message || err);
|
|
});
|
|
}
|
|
broadcast({
|
|
type: 'device_id_changed',
|
|
old_id: oldId,
|
|
new_id: newId,
|
|
source: payload.source || '',
|
|
timestamp: event.timestamp || Date.now(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'device_pending' || event.type === 'enrollment_pending') {
|
|
broadcastPanelEvent({
|
|
type: 'registration_pending',
|
|
registration: payload,
|
|
timestamp: event.timestamp || Date.now(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'device_approved' || event.type === 'device_rejected' ||
|
|
event.type === 'enrollment_rejection_cleared') {
|
|
broadcastPanelEvent({
|
|
type: 'registration_changed',
|
|
registration: payload,
|
|
timestamp: event.timestamp || Date.now(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'help_request') {
|
|
broadcastPanelEvent({
|
|
type: 'help_request',
|
|
...payload,
|
|
timestamp: event.timestamp || Date.now(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Forward peer status events to browser clients
|
|
if (event.type === 'peer_online' || event.type === 'peer_offline' ||
|
|
event.type === 'peer_status_changed' || event.type === 'peer_registered') {
|
|
broadcast({
|
|
type: 'device_status',
|
|
device_id: payload.peer_id || payload.id || event.peer_id || event.id || event.device_id,
|
|
status: payload.status || event.status || (event.type === 'peer_online' ? 'online' : 'offline'),
|
|
timestamp: event.timestamp || Date.now(),
|
|
details: event,
|
|
});
|
|
}
|
|
} catch (_) {
|
|
// Ignore unparseable frames
|
|
}
|
|
});
|
|
|
|
goWs.on('close', () => {
|
|
log.warn('Go event bus disconnected, retrying in ' + retryDelay + 'ms');
|
|
setTimeout(connectToGoEventBus, retryDelay);
|
|
retryDelay = Math.min(retryDelay * 2, RECONNECT_MAX);
|
|
});
|
|
|
|
goWs.on('error', (err) => {
|
|
const msg = err.message || '';
|
|
// Detect TLS mismatch (Go server has TLS_API=Y but we connect via ws://) — issue #104
|
|
if (msg.includes('unexpected server response: 400') || msg.includes('Parse Error') ||
|
|
msg.includes('ECONNRESET') || msg.includes('socket hang up')) {
|
|
log.error('Go event bus error: ' + msg +
|
|
' — If Go server has TLS_API=Y enabled, the API port must stay HTTP. See issue #104.');
|
|
} else {
|
|
log.error('Go event bus error:', msg);
|
|
}
|
|
goWs.close();
|
|
});
|
|
}
|
|
|
|
// Only connect if we have the Go API URL
|
|
if (goApiUrl && apiKey) {
|
|
connectToGoEventBus();
|
|
} else {
|
|
log.warn('Go API URL or API key not configured, device status push disabled');
|
|
}
|
|
|
|
log.info('Device status push initialized');
|
|
return wss;
|
|
}
|
|
|
|
module.exports = { initDeviceStatusPush, publishPanelEvent };
|