/**
* @module DxlClient
* @description Implementation of the `dxl-client` node
*/
'use strict'
var fs = require('fs')
var path = require('path')
var dxl = require('@opendxl/dxl-client')
var Client = dxl.Client
var Config = dxl.Config
var ServiceRegistrationInfo = dxl.ServiceRegistrationInfo
var NodeUtils = require('../lib/node-utils')
var DEFAULT_CONFIG_FILE_NAME = 'dxlclient.config'
/**
* @classdesc Responsible for all communication with the
* Data Exchange Layer (DXL) fabric.
* @external DxlClient
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Client.html}
*/
/**
* @classdesc Event messages are sent using the {@link DxlClient#sendEvent} method
* of a client instance. Event messages are sent by one publisher and received
* by one or more recipients that are currently subscribed to the topic
* associated with the event (otherwise known as one-to-many).
* @external Event
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Event.html}
*/
/**
* @classdesc Request messages are used when invoking a method on a remote
* service. This communication is one-to-one where a client sends a request to
* a service instance and in turn receives a response.
* @external Request
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Request.html}
*/
/**
* @classdesc Response messages are sent by service instances upon
* receiving [Request]{@link external:Request} messages. This communication is
* one-to-one where a client sends a request to a service instance and in turn
* receives a response.
* @external Response
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Response.html}
*/
/**
* @classdesc Service registration instances are used to register and expose
* services onto a DXL fabric.
* @external ServiceRegistrationInfo
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/ServiceRegistrationInfo.html}
*/
/**
* @classdesc A general Data Exchange Layer (DXL) exception.
* @external DxlError
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/DxlError.html}
*/
module.exports = function (RED) {
/**
* @classdesc Client configuration node responsible for establishing
* communication with the Data Exchange Layer (DXL) fabric.
* @param {Object} nodeConfig - Configuration data which the node uses.
* @param {String} nodeConfig.configFile - Path to the DXL client
* configuration file. If this value is a directory, the node will attempt
* to load a file named `dxlclient.config` within the directory.
* @param {Number} nodeConfig.keepAliveInterval - The maximum period in
* seconds between communications with a connected broker. If no other
* messages are being exchanged, this controls the rate at which the client
* will send ping messages to the broker.
* @param {Number} nodeConfig.reconnectDelay - The delay between
* connection retry attempts in seconds.
* @constructor
*/
function DxlClientNode (nodeConfig) {
RED.nodes.createNode(this, nodeConfig)
var configFile = nodeConfig.configFile
if (fs.statSync(configFile).isDirectory()) {
var configFileWithDefault = path.join(configFile,
DEFAULT_CONFIG_FILE_NAME)
if (fs.existsSync(configFileWithDefault)) {
configFile = configFileWithDefault
}
}
var clientConfig = Config.createDxlConfigFromFile(configFile)
clientConfig.keepAliveInterval = NodeUtils.valueToNumber(
nodeConfig.keepAliveInterval, 1800)
clientConfig.reconnectDelay = NodeUtils.valueToNumber(
nodeConfig.reconnectDelay, 1)
/**
* Whether or not the client is currently connected to the DXL fabric.
* @type {boolean}
*/
this.connected = false
/**
* Handle to the underlying DXL client object
* @type {external:DxlClient}
*/
this.dxlClient = new Client(clientConfig)
/**
* Whether or not the DXL client is in the process of connecting to the
* DXL fabric.
* @type {boolean}
* @private
*/
this._connecting = false
/**
* Whether or not this node is in the process of being closed.
* @type {boolean}
* @private
*/
this._closing = false
/**
* Object containing information about the nodes which are currently using
* this configuration node. This is used to determine when it is necessary
* to try to connect to the DXL fabric and to update the node's status as
* the broker connection state changes. Each object's key is a node id and
* corresponding value is the node object.
* @type {boolean}
* @private
*/
this._users = {}
var node = this
/**
* Attempts to connect the client to the DXL fabric.
* @name DxlClientNode#_connect
* @private
*/
this._connect = function () {
if (!node.connected && !node._connecting) {
node._connecting = true
node.dxlClient.connect()
node.dxlClient.setMaxListeners(0)
// Register successful connect or reconnect handler
node.dxlClient.on('connect', function () {
node._connecting = false
node.connected = true
for (var id in node._users) {
if (node._users.hasOwnProperty(id)) {
node._users[id].status({
fill: 'green',
shape: 'dot',
text: 'node-red:common.status.connected'
})
}
}
})
node.dxlClient.on('reconnect', function () {
for (var id in node._users) {
if (node._users.hasOwnProperty(id)) {
node._users[id].status({
fill: 'yellow',
shape: 'ring',
text: 'node-red:common.status.connecting'
})
}
}
})
// Register disconnect handlers
node.dxlClient.on('close', function () {
if (node.connected) {
node.connected = false
for (var id in node._users) {
if (node._users.hasOwnProperty(id)) {
node._users[id].status({
fill: 'red',
shape: 'ring',
text: 'node-red:common.status.disconnected'
})
}
}
} else if (node._connecting) {
node.log('Connect failed')
}
})
}
}
/**
* Register the supplied node as a "user" of the client config node. This is
* used to determine when it is necessary to attempt to connect to the DXL
* fabric and when the client could be disconnected (when no users are
* remaining). The registered node's status method is called back upon in
* order to update the node with the current status information for the
* broker connection.
* @param {Object} userNode - The node to register.
*/
this.registerUserNode = function (userNode) {
node._users[userNode.id] = userNode
if (Object.keys(node._users).length === 1) {
node._connect()
}
}
/**
* Unregister the supplied node as a "user" of the client config node. This
* is used to determine when it is necessary to attempt to connect to the
* DXL fabric and when the client could be disconnected (when no users are
* remaining). The registered node's status method is called back upon in
* order to update the node with the current status information for the
* broker connection.
*/
this.unregisterUserNode = function (userNode, done) {
delete node._users[userNode.id]
if (node._closing) {
return done()
}
if (Object.keys(node._users).length === 0) {
if (node.dxlClient && node.dxlClient.connected) {
return node.dxlClient.disconnect(done)
} else {
node.dxlClient.disconnect()
return done()
}
}
done()
}
/**
* Adds an event callback to the client for the specified topic. The
* callback will be invoked when [DxlClient]{@link external:DxlClient}
* messages are received by the client on the specified topic.
* @param {String} topic - Topic to receive
* [DxlClient]{@link external:DxlClient} messages on. An empty string or
* null value indicates that the callback should receive messages for all
* topics (no filtering).
* @param {Function} eventCallback - Callback function which should be
* invoked for a matching message. The first argument passed to the
* callback function is the [Event]{@link external:Event} object.
*/
this.addEventCallback = function (topic, eventCallback) {
node.dxlClient.addEventCallback(topic, eventCallback)
}
/**
* Removes an event callback from the client for the specified topic. This
* method must be invoked with the same arguments as when the callback was
* originally registered via
* [addEventCallback]{@link module:DxlClient~DxlClientNode#addEventCallback}.
* @param {String} topic - The topic to remove the callback for.
* @param {Function} eventCallback - The event callback to be removed for
* the specified topic.
*/
this.removeEventCallback = function (topic, eventCallback) {
if (!node._closing) {
node.dxlClient.removeEventCallback(topic, eventCallback)
}
}
/**
* Sends a [Request]{@link external:Request} message to a remote DXL service
* asynchronously. An optional response callback can be specified. This
* callback will be invoked when the corresponding
* [Response]{@link external:Response} message is received by the client.
* @param {external:Request} request - The request message to send to a
* remote DXL service.
* @param {Function} [responseCallback] - An optional response callback
* that will be invoked when the corresponding
* [Response]{@link external:Response} message is received by the client.
* @throws {external:DxlError} If no prior attempt has been made to connect
* the client. This could occur if no prior call has been made to
* [registerUserNode]{@link module:DxlClient~DxlClientNode#registerUserNode}.
*/
this.asyncRequest = function (request, responseCallback) {
node.dxlClient.asyncRequest(request, responseCallback)
}
/**
* Attempts to deliver the specified [Event]{@link external:Event} message
* to the DXL fabric.
* @param {external:Event} event - The {@link external:Event} to send.
* @throws {external:DxlError} If no prior attempt has been made to connect
* the client. This could occur if no prior call has been made to
* [registerUserNode]{@link module:DxlClient~DxlClientNode#registerUserNode}.
*/
this.sendEvent = function (event) {
node.dxlClient.sendEvent(event)
}
/**
* Attempts to deliver the specified [Response]{@link external:Response}
* message to the DXL fabric. The fabric will in turn attempt to deliver the
* response back to the client who sent the corresponding
* [Request]{@link external:Request}.
* @param {external:Response} response - The
* [Response]{@link external:Response} to send.
* @throws {external:DxlError} If no prior attempt has been made to connect
* the client. This could occur if no prior call has been made to
* [registerUserNode]{@link module:DxlClient~DxlClientNode#registerUserNode}.
*/
this.sendResponse = function (response) {
node.dxlClient.sendResponse(response)
}
/**
* Registers a DXL service with the fabric asynchronously.
* @param {String} serviceType - A textual name for the service. For
* example, '/mycompany/myservice'.
* @param {Object} callbacksByTopic - Object containing a set of topics for
* the service to respond to along with their associated request callback
* instances. Each key in the object should have a string representation
* of the topic name. Each corresponding value in the object should
* contain the function to be invoked when a
* [Request]{@link external:Request} message is received. The
* [Request]{@link external:Request} object is supplied as the only
* parameter to the request callback function.
* @returns {external:ServiceRegistrationInfo} An object containing
* information for the registered service. This value should be supplied
* in the corresponding call to
* [unregisterServiceAsync]{@link module:DxlClient~DxlClientNode#unregisterServiceAsync}.
* when the service should be unregistered.
*/
this.registerServiceAsync = function (serviceType, callbacksByTopic) {
var serviceInfo = new ServiceRegistrationInfo(node.dxlClient,
serviceType)
serviceInfo.addTopics(callbacksByTopic)
node.dxlClient.registerServiceAsync(serviceInfo)
return serviceInfo
}
/**
* Unregisters (removes) a DXL service from the fabric asynchronously. The
* specified
* [ServiceRegistrationInfo]{@link external:ServiceRegistrationInfo}
* instance contains information about the service that is to be removed.
* @param {external:ServiceRegistrationInfo} serviceRegInfo - A
* [ServiceRegistrationInfo]{@link external:ServiceRegistrationInfo}
* instance containing information about the service that is to be
* unregistered.
*/
this.unregisterServiceAsync = function (serviceRegInfo) {
node.dxlClient.unregisterServiceAsync(serviceRegInfo)
}
this.on('close', function (done) {
node._closing = true
if (this.connected) {
node.dxlClient.once('close', function () { done() })
}
node.dxlClient.destroy()
if (!this.connected) {
done()
}
})
}
RED.nodes.registerType('dxl-client', DxlClientNode)
RED.httpAdmin.post('/dxl-client/provision-config',
RED.auth.needsPermission('dxl-client.write'), function (request, response) {
var body = request.body
if (typeof body === 'object') {
try {
Config.provisionConfig(body.configDir, body.commonOrCsrFileName,
body.hostInfo,
{
doneCallback: function (error) {
if (error) {
response.status(500).send(error.message)
} else {
response.sendStatus(200)
}
}
})
} catch (err) {
response.status(500).send(err.message)
}
} else {
response.status(400).send('Request body missing')
}
}
)
RED.httpAdmin.get('/dxl-client/defaults',
function (request, response) {
response.status(200).send({
configDir: path.join(RED.settings.userDir || '', 'dxl')
})
}
)
RED.httpAdmin.get('/dxl-client/provisioned-files',
function (request, response) {
var existingFiles = []
var configDir = request.query.configDir
if (fs.existsSync(configDir)) {
var filesToCheck = [
'ca-bundle.crt', 'client.crt', 'client.csr',
'client.key', 'dxlclient.config']
filesToCheck.forEach(function (fileToCheck) {
if (fs.existsSync(path.join(configDir, fileToCheck))) {
existingFiles.push(fileToCheck)
}
})
}
response.status(200).send(existingFiles)
}
)
}