@mdf.js/openc2
Version:
MMS - API - Observability
207 lines • 9.08 kB
JavaScript
"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