UNPKG

@mdf.js/openc2-core

Version:

MMS - API Core - OpenC2

356 lines 17.3 kB
"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