@mdf.js/openc2-core
Version:
MMS - API Core - OpenC2
356 lines • 17.3 kB
JavaScript
"use strict";
/**
* Copyright 2024 Mytra Control S.L. All rights reserved.
*
* Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
* or at https://opensource.org/licenses/MIT.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Producer = void 0;
const crash_1 = require("@mdf.js/crash");
const lodash_1 = require("lodash");
const helpers_1 = require("../../helpers");
const types_1 = require("../../types");
const Component_1 = require("../Component");
const ConsumerMap_1 = require("./ConsumerMap");
const core_1 = require("./core");
const DEFAULT_AGING_CHECK_INTERVAL = 1000 * 60; // 1 minute
const DEFAULT_MAX_AGE = DEFAULT_AGING_CHECK_INTERVAL * 3; // 3 minutes
class Producer extends Component_1.Component {
/**
* Regular OpenC2 producer implementation.
* @param adapter - transport adapter
* @param options - configuration options
*/
constructor(adapter, options) {
var _a, _b;
super(new core_1.AdapterWrapper(adapter, options.retryOptions), options);
/**
* Process incoming command message from the router
* @param incomingMessage - incoming message
* @param done - callback to return the response
*/
this.onCommandHandler = (incomingMessage, done) => {
this.command(incomingMessage)
.then(result => done(undefined, result))
.catch(error => done(error, undefined));
};
/** Perform the lookup of OpenC2 consumers */
this.lookup = () => {
var _a;
const command = helpers_1.Helpers.queryFeatures((_a = this.options.lookupTimeout) !== null && _a !== void 0 ? _a : 30000);
// Stryker disable next-line all
this.logger.debug(`New lookup command will be emitted`);
this.command(['*'], command)
.then(responses => {
this.consumerMap.update(responses);
})
.catch(rawError => {
const error = crash_1.Crash.from(rawError);
this.onErrorHandler(new crash_1.Crash(`Error performing a new lookup: ${error.message}`, this.componentId, {
cause: rawError,
}));
});
};
/**
* Handle errors processing incoming messages
* @param rawError - raw error
*/
this.onProcessingMessageError = (rawError) => {
const error = crash_1.Crash.from(rawError);
const processingError = new crash_1.Crash(`Error processing incoming response message from control chanel: ${error.message}`, this.componentId, { cause: error });
this.onErrorHandler(processingError);
};
this.consumerMap = new ConsumerMap_1.ConsumerMap(this.name, (_a = this.options.agingInterval) !== null && _a !== void 0 ? _a : DEFAULT_AGING_CHECK_INTERVAL, (_b = this.options.maxAge) !== null && _b !== void 0 ? _b : DEFAULT_MAX_AGE);
this._router.on('command', this.onCommandHandler);
this.health.add(this.consumerMap);
// Stryker disable next-line all
this.logger.debug(`OpenC2 Producer created - [${options.id}]`);
}
/** Initialize the OpenC2 component */
startup() {
if (!this.lookupTimer && this.options.lookupInterval && this.options.lookupTimeout) {
this.lookupTimer = setInterval(this.lookup, this.options.lookupInterval);
}
return Promise.resolve();
}
/** Shutdown the OpenC2 component */
shutdown() {
if (this.lookupTimer) {
clearInterval(this.lookupTimer);
this.lookupTimer = undefined;
}
return Promise.resolve();
}
async command(to, content, target) {
var _a;
try {
const command = this.getCommand(to, content, target);
// Stryker disable all
this.logger.debug(`Request for command: ${command.request_id}`);
this.logger.silly(`Direction: ${command.from} - ${command.to}`);
this.logger.silly(`Command: ${command.content.action} - ${JSON.stringify(command.content.target, null, 2)}`);
// Stryker enable all
if (((_a = command.content.args) === null || _a === void 0 ? void 0 : _a.response_requested) !== types_1.Control.ResponseType.None) {
return this.waitForResponse(command);
}
else {
// Stryker disable next-line all
this.logger.debug(`Response not requested for command: ${command.request_id}`);
await this.adapter.publish(command);
return [];
}
}
catch (error) {
this.onErrorHandler(error);
throw error;
}
}
/**
* Get and validate the command message to be published
* @param destinationsOrCommand - destination of command defined in content or command itself
* @param contentOrAction - Command to be issued
* @param id - producer identification
* @returns
*/
getCommand(destinationsOrCommand, contentOrAction, target) {
let message;
if (!Array.isArray(destinationsOrCommand) &&
contentOrAction === undefined &&
destinationsOrCommand) {
message = destinationsOrCommand;
}
else if (Array.isArray(destinationsOrCommand) &&
typeof contentOrAction === 'object' &&
target === undefined) {
message = helpers_1.Helpers.createCommand(destinationsOrCommand, contentOrAction, this.options.id);
}
else if (Array.isArray(destinationsOrCommand) &&
typeof contentOrAction === 'string' &&
typeof target === 'object') {
message = helpers_1.Helpers.createCommandByAction(destinationsOrCommand, contentOrAction, target, this.options.id);
}
else {
throw new crash_1.Crash(`Invalid type of parameters in command creation`, this.componentId);
}
return helpers_1.Checkers.isValidCommandSync(message, this.adapter.componentId);
}
/**
* Wait for the response to an issued command
* @param command - issued command
*/
waitForResponse(command) {
if (command.to.includes('*') ||
command.to.length > 1 ||
helpers_1.Accessors.getActuatorsFromCommandMessage(command).length > 0) {
return this.waitForBroadCastResponses(command);
}
else {
return Promise.all(command.to.map(consumer => {
const singleConsumerCommand = (0, lodash_1.cloneDeep)(command);
singleConsumerCommand.to = [consumer];
return this.waitForConsumerResponse(singleConsumerCommand);
}));
}
}
/**
* Wait for the response of several consumers during the defined duration in the command
* @param command - issued command
* @returns
*/
waitForBroadCastResponses(command) {
return new Promise((resolve, reject) => {
const requestId = command.request_id;
const responses = [];
const timeout = helpers_1.Accessors.getDelayFromCommandMessage(command);
// Stryker disable next-line all
this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`);
// *******************************************************************************************
// #region Incoming response handler
const broadcastResponseHandler = this.getBroadcastResponseHandler(command, responses);
// #endregion
// *******************************************************************************************
// #region On timeout event handler
const onTimeout = () => {
// Stryker disable next-line all
this.logger.debug(`Timeout complete for command: ${requestId}, all the responses will be resolved`);
this.adapter.off(requestId, broadcastResponseHandler);
resolve(responses);
};
// #endregion
// *******************************************************************************************
// #region On directly responses to publish method
const onDirectResponse = (result) => {
if (result) {
const directResponses = Array.isArray(result) ? result : [result];
for (const response of directResponses) {
broadcastResponseHandler(response);
}
this.adapter.off(requestId, broadcastResponseHandler);
clearTimeout(timeoutTimer);
resolve(directResponses);
}
};
// #endregion
// *******************************************************************************************
// #region On error on publish method
const onPublishError = (rawError) => {
clearTimeout(timeoutTimer);
this.adapter.off(requestId, broadcastResponseHandler);
const error = crash_1.Crash.from(rawError);
const publishingError = new crash_1.Crash(`Error publishing command to control channel: ${error.message}`, this.componentId, { cause: error });
this.onErrorHandler(publishingError);
reject(publishingError);
};
// #endregion
// *******************************************************************************************
// #region Subscription to responses
this.adapter.on(requestId, broadcastResponseHandler);
const timeoutTimer = setTimeout(onTimeout, timeout);
this.adapter.publish(command).then(onDirectResponse).catch(onPublishError);
});
}
/**
* Wait for the response of several consumers during the defined duration in the command
* @param command - issued command
* @param responses - responses array
*/
getBroadcastResponseHandler(command, responses) {
const requestId = command.request_id;
return (incomingMessage) => {
var _a;
try {
const message = helpers_1.Checkers.isValidResponseSync(incomingMessage, this.componentId);
// Stryker disable next-line all
this.logger.debug(`New message from ${message.from} - ${message.request_id}`);
if (!helpers_1.Checkers.isResponseToInstance(message, command.from, requestId)) {
// Stryker disable next-line all
this.logger.debug(`${message.request_id} is not a response for this instance`);
return;
}
else if (message.request_id === requestId) {
this.register.push(message);
if (message.status >= 200) {
// Stryker disable next-line all
this.logger.debug(`Response from: [${message.from}] received`);
responses.push(message);
}
else {
// Stryker disable next-line all
this.logger.debug(`ACK from: [${message.from}] received`);
if (((_a = command.content.args) === null || _a === void 0 ? void 0 : _a.response_requested) === types_1.Control.ResponseType.ACK) {
responses.push(message);
}
}
}
}
catch (rawError) {
this.onProcessingMessageError(rawError);
}
};
}
/**
* Wait for the response to an issued command from one consumer
* @param command - issued command
*/
waitForConsumerResponse(command) {
return new Promise((resolve, reject) => {
const requestId = command.request_id;
const timeout = helpers_1.Accessors.getDelayFromCommandMessage(command);
// Stryker disable next-line all
this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`);
// *******************************************************************************************
// #region Incoming response handler
const consumerResponseHandler = (incomingMessage) => {
try {
const message = helpers_1.Checkers.isValidResponseSync(incomingMessage, this.componentId);
// Stryker disable next-line all
this.logger.debug(`New message from ${message.from} - ${message.request_id}`);
if (!helpers_1.Checkers.isResponseToInstance(message, command.from, requestId)) {
// Stryker disable next-line all
this.logger.debug(`${message.request_id} is not a response for this instance`);
}
else if (message.request_id === requestId) {
try {
const result = this.handlerMessage(message, command);
if (result) {
clearTimeout(timeoutTimer);
this.adapter.off(requestId, consumerResponseHandler);
resolve(result);
}
}
catch (rawError) {
clearTimeout(timeoutTimer);
this.adapter.off(requestId, consumerResponseHandler);
reject(crash_1.Crash.from(rawError));
}
}
}
catch (rawError) {
this.onProcessingMessageError(rawError);
}
};
// #endregion
// *******************************************************************************************
// #region On timeout event handler
const onTimeout = () => {
const timeOutError = new crash_1.Crash(`Response timeout for the command ${requestId} [${command.content.action}]`, requestId);
this.logger.debug(timeOutError.message);
this.adapter.off(requestId, consumerResponseHandler);
reject(timeOutError);
};
// #endregion
// *******************************************************************************************
// #region On directly responses to publish method
const onDirectResponse = (result) => {
if (Array.isArray(result)) {
clearTimeout(timeoutTimer);
this.adapter.off(requestId, consumerResponseHandler);
reject(new crash_1.Crash(`Command to a single destination was resolved with multiple responses: ${result.length}`, requestId, { info: result }));
}
else if (result) {
consumerResponseHandler(result);
}
};
// #endregion
// *******************************************************************************************
// #region On error on publish method
const onPublishError = (rawError) => {
clearTimeout(timeoutTimer);
this.adapter.off(requestId, consumerResponseHandler);
const error = crash_1.Crash.from(rawError);
const publishingError = new crash_1.Crash(`Error publishing command to control channel: ${error.message}`, this.componentId, { cause: error });
this.onErrorHandler(publishingError);
reject(publishingError);
};
// #endregion
this.adapter.on(requestId, consumerResponseHandler);
const timeoutTimer = setTimeout(onTimeout, timeout);
this.adapter.publish(command).then(onDirectResponse).catch(onPublishError);
});
}
handlerMessage(message, command) {
var _a;
this.register.push(message);
if (message.status >= 200 && message.status < 300) {
// Stryker disable next-line all
this.logger.debug(`Response from: [${message.from}] received - Fulfilled`);
return message;
}
else if (message.status >= 300) {
// Stryker disable next-line all
this.logger.warn(`Response from: [${message.from}] received - Not fulfilled`);
throw new crash_1.Crash(`Command was not fulfilled: [status ${message.status}]`, message.request_id);
}
else {
// Stryker disable next-line all
this.logger.debug(`ACK from: [${message.from}] received`);
if (((_a = command.content.args) === null || _a === void 0 ? void 0 : _a.response_requested) === types_1.Control.ResponseType.ACK) {
return message;
}
return undefined;
}
}
}
exports.Producer = Producer;
//# sourceMappingURL=Producer.js.map