signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
135 lines • 6.94 kB
JavaScript
;
/**
* Binary Stream WebSocket Endpoints
*
* Provides WebSocket endpoints for high-frequency binary data streaming
* from WASM plugins (radar spokes, AIS targets, etc.)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.binaryStreamManager = void 0;
exports.initializeBinaryStreams = initializeBinaryStreams;
const debug_1 = __importDefault(require("debug"));
const ws_1 = __importDefault(require("ws"));
const cookie_1 = __importDefault(require("cookie"));
const binary_stream_manager_1 = require("./binary-stream-manager");
Object.defineProperty(exports, "binaryStreamManager", { enumerable: true, get: function () { return binary_stream_manager_1.binaryStreamManager; } });
const debug = (0, debug_1.default)('signalk:streams');
/**
* Initialize binary stream WebSocket endpoints
*
* @param app - SignalK application instance
*/
function initializeBinaryStreams(app) {
debug('initializeBinaryStreams called, app.server exists: %s', !!app.server);
if (!app.server) {
debug('HTTP server not available, skipping binary stream initialization');
return;
}
debug('Initializing binary stream WebSocket endpoints');
// Handle WebSocket upgrade requests for binary streams
// Note: This listener is added to app.server which should be an HTTP server
debug('Adding upgrade listener to server');
app.server.on('upgrade', (request, socket, head) => {
debug('Upgrade request received: %s, headers.host: %s', request.url, request.headers.host);
try {
const url = new URL(request.url, `http://${request.headers.host}`);
const pathname = url.pathname;
debug('Pathname: %s', pathname);
// Match: /signalk/v2/api/streams/:streamId (support path segments in streamId)
const streamMatch = pathname.match(/^\/signalk\/v2\/api\/streams\/(.+)$/);
// Match: /signalk/v2/api/vessels/self/radars/:id/stream (convenience alias)
const radarMatch = pathname.match(/^\/signalk\/v2\/api\/vessels\/self\/radars\/([^\/]+)\/stream$/);
let streamId = null;
if (streamMatch) {
streamId = decodeURIComponent(streamMatch[1]);
debug('Matched stream pattern: %s', streamId);
}
else if (radarMatch) {
// Alias: map radar endpoint to radars/{radarId} stream
const radarId = radarMatch[1];
streamId = `radars/${radarId}`;
debug('Matched radar stream pattern: %s', streamId);
}
else {
// Not a binary stream endpoint, let other handlers process
debug('No match for path, ignoring: %s', pathname);
return;
}
debug('Processing WebSocket upgrade for stream: %s', streamId);
// Authenticate the request (if security is enabled)
let principal = { identifier: 'unknown' };
const authRequest = request;
// Upgrade requests skip Express, so parse the cookie header and query
// string onto the request the way the middleware chain would. Without
// this authorizeWS() finds no token and rejects every browser
// connection — the HttpOnly JAUTHENTICATION cookie the browser sends
// on a same-origin upgrade would otherwise be ignored. Mirrors the
// v1 stream path in interfaces/ws.ts.
const cookieHeader = request.headers.cookie;
if (typeof cookieHeader === 'string') {
authRequest.cookies = cookie_1.default.parse(cookieHeader);
}
authRequest.query = Object.fromEntries(url.searchParams);
// Check if security is enabled
if (app.securityStrategy &&
typeof app.securityStrategy.shouldAllowWrite === 'function') {
try {
// Security is enabled, perform authentication
if (app.securityStrategy.authorizeWS) {
app.securityStrategy.authorizeWS(request);
principal = authRequest.skPrincipal || { identifier: 'unknown' };
}
else {
// Fallback: use shouldAllowWrite for basic auth check
if (!app.securityStrategy.shouldAllowWrite(request, 'streams')) {
throw new Error('Unauthorized');
}
principal = authRequest.skPrincipal || { identifier: 'unknown' };
}
}
catch (error) {
debug(`Authentication failed for stream ${streamId}: ${error}`);
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
}
else {
// Security is disabled, allow connection without authentication
debug(`Security disabled, allowing unauthenticated connection to stream: ${streamId}`);
principal = { identifier: 'unauthenticated' };
}
// Create WebSocket connection
debug('Creating WebSocket server for stream: %s', streamId);
const wss = new ws_1.default.Server({ noServer: true });
wss.handleUpgrade(request, socket, head, (ws) => {
debug('WebSocket connected to stream: %s', streamId);
// Register client with stream manager
binary_stream_manager_1.binaryStreamManager.addClient(streamId, ws, principal);
ws.on('close', () => {
debug(`WebSocket disconnected from stream: ${streamId}`);
binary_stream_manager_1.binaryStreamManager.removeClient(streamId, ws);
});
ws.on('error', (err) => {
debug(`WebSocket error for stream ${streamId}: ${err}`);
binary_stream_manager_1.binaryStreamManager.removeClient(streamId, ws);
});
// Binary streams are one-way (server → client), ignore client messages
ws.on('message', () => {
debug(`Ignoring message from client on binary stream ${streamId}`);
});
});
}
catch (error) {
debug(`Error handling WebSocket upgrade: ${error}`);
console.error('[signalk:streams] WebSocket upgrade error:', error);
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
socket.destroy();
}
});
debug('Binary stream WebSocket endpoints initialized');
}
//# sourceMappingURL=index.js.map