UNPKG

@mdf.js/openc2

Version:

MMS - API - Observability

207 lines 9.08 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceBus = void 0; const tslib_1 = require("tslib"); const Health_1 = require("@mdf.js/core/dist/Health"); const crash_1 = require("@mdf.js/crash"); const openc2_core_1 = require("@mdf.js/openc2-core"); const socket_server_provider_1 = require("@mdf.js/socket-server-provider"); const events_1 = tslib_1.__importDefault(require("events")); const os_1 = tslib_1.__importDefault(require("os")); const uuid_1 = require("uuid"); const AddressMapper_1 = require("./AddressMapper"); const Events_1 = require("./Events"); const middlewares_1 = require("./middlewares"); const ALLOWED_DISCONNECT_REASONS = [ 'server namespace disconnect', 'client namespace disconnect', 'server shutting down', ]; const SUBJECT = 'Socket.IO OpenC2'; class ServiceBus extends events_1.default { /** * Create a new ServiceBus instance * @param serverOptions - Socket.IO server options * @param options - Socket.IO client configuration options * @param name - name of the service bus */ constructor(serverOptions, options, name) { super(); this.name = name; /** Component identification */ this.componentId = (0, uuid_1.v4)(); /** * Connection event handler for the OpenC2 namespace * @param socket - socket to be configured */ this.onConnectionEventOC2Namespace = (socket) => { this.addressMapper.update(socket.id, socket.handshake.auth['nodeId']); if (socket.handshake.auth['type'] === 'producer') { socket.join('producer'); } else if (socket.handshake.auth['type'] === 'consumer') { socket.join('consumer'); for (const actuator of socket.handshake.auth['actuators']) { socket.join(actuator); } } socket.onAny(this.eventHandler); socket.on('disconnect', this.onDisconnectEvent); }; /** * Manage the incoming commands events from provider * @param event - event name * @param command - command message from provider * @param callback - callback function, used as acknowledgement */ this.eventHandler = (event, command, callback) => { const wrappedCallback = (error, responses) => { if (error) { this.onErrorHandler(new crash_1.Crash(`Error in the acknowledgement callback function: ${error.message}`, this.componentId, { info: { event, command, subject: SUBJECT } })); } else if (!responses || (Array.isArray(responses) && responses.length === 0)) { this.onErrorHandler(new crash_1.Crash('No responses returned in the acknowledgement callback function', this.componentId, { info: { event, command, subject: SUBJECT } })); } else { callback(responses); } }; // Command to oc2/cmd/all is sent to all consumers if (Events_1.Events.isGeneralCommandEvent(event)) { this.oc2Namespace .in('consumer') .timeout(openc2_core_1.Accessors.getDelayFromCommandMessage(command)) .emit(event, command, wrappedCallback); } // Command to oc2/cmd/ap/[actuator_profile] is sent to consumers that have this profile else if (Events_1.Events.isActuatorCommandEvent(event)) { this.oc2Namespace .in(Events_1.Events.getActuatorFromCommandEvent(event)) .timeout(openc2_core_1.Accessors.getDelayFromCommandMessage(command)) .emit(event, command, wrappedCallback); } // Command to oc2/cmd/device/[device_id] is sent to the consumer that has this id else if (Events_1.Events.isDeviceCommandEvent(event)) { const openC2Id = Events_1.Events.getDeviceFromCommandEvent(event); const socketId = this.addressMapper.getByOpenC2Id(openC2Id); if (socketId) { this.oc2Namespace .to(socketId) .timeout(openc2_core_1.Accessors.getDelayFromCommandMessage(command)) .emit(event, command, wrappedCallback); } } else { this.onErrorHandler(new crash_1.Crash(`Invalid command from or message in OpenC2 Socket.IO Server: ${event}`, this.componentId, { info: { event, command, subject: SUBJECT } })); } }; /** Socket disconnection handler */ this.onDisconnectEvent = (socketId) => { return (reason) => { const openC2Id = this.addressMapper.getBySocketId(socketId); const error = this.disconnectReasonToCrashError(reason, openC2Id !== null && openC2Id !== void 0 ? openC2Id : 'unknown'); this.addressMapper.delete(socketId); if (error) { this.onErrorHandler(error); } }; }; /** * Manage the error in the service bus * @param error - error to be processed */ this.onErrorHandler = (error) => { const crash = crash_1.Crash.from(error); if (this.listenerCount('error') > 0) { this.emit('error', crash); } }; /** * Manage the status change in the service bus * @param status - status to be processed */ this.onStatusHandler = (status) => { if (this.listenerCount('status') > 0) { this.emit('status', status); } }; this.addressMapper = new AddressMapper_1.AddressMapper(); this.instance = socket_server_provider_1.SocketIOServer.Factory.create({ config: { ...serverOptions, transports: ['websocket'] }, name, }); this.oc2Namespace = this.instance.client.of('/openc2'); this.oc2Namespace.use(middlewares_1.Check.handler()); if (options.useJwt) { this.oc2Namespace.use(middlewares_1.AuthZ.handler({ secret: options.secret })); } this.oc2Namespace.on('connection', this.onConnectionEventOC2Namespace); this.instance.on('error', this.onErrorHandler); this.instance.on('status', this.onStatusHandler); } /** * Transforms the disconnection reason in a Crash if its an unmanaged reason * @param reason - reason for the error * @param openC2Id - openC2 identification * @returns */ disconnectReasonToCrashError(reason, openC2Id) { if (!ALLOWED_DISCONNECT_REASONS.includes(reason)) { return new crash_1.Crash(`OpenC2 node ${openC2Id} has been disconnected due to: ${reason}`, this.componentId, { info: { openC2Id: openC2Id, subject: SUBJECT } }); } else { return undefined; } } /** Start the underlayer Socket.IO server */ start() { return this.instance.start(); } /** Close the server and disconnect all the actual connections */ stop() { this.instance.client.disconnectSockets(); return this.instance.stop(); } /** Close the server and disconnect all the actual connections */ close() { this.instance.client.disconnectSockets(); return this.instance.close(); } /** Return the status of the server */ get status() { return (0, Health_1.overallStatus)(this.checks); } /** * Return the status of the server in a standard format * @returns _check object_ as defined in the draft standard * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 */ get checks() { return { ...this.instance.checks, [`${this.name}:serverStats`]: [ { componentId: this.componentId, componentName: this.name, componentType: 'server', observedValue: { hostname: os_1.default.hostname(), pid: process.pid, uptime: process.uptime(), clientsCount: this.instance.client.engine.clientsCount, pollingClientsCount: this.instance.client._pollingClientsCount, namespaces: Array.from(this.instance.client._nsps.values()).map(nsp => ({ name: nsp.name, socketsCount: nsp.sockets.size, })), }, observedUnit: 'stats', status: 'pass', time: new Date().toISOString(), }, ], }; } } exports.ServiceBus = ServiceBus; //# sourceMappingURL=ServiceBus.js.map