'use strict'
var fs = require('fs')
var path = require('path')
var Broker = require('./broker')
var DxlError = require('./dxl-error')
var MalformedBrokerError = require('./malformed-broker-error')
var util = require('./util')
var provisionConfig = require('./_provisioning/provision-config')
var Proxy = require('./proxy')
var DEFAULT_MQTT_KEEP_ALIVE_INTERVAL = 30 * 60 // seconds
var DEFAULT_RECONNECT_DELAY = 1 // seconds
/**
* Get the value for a setting from a configuration file.
* @private
* @param {Object} config - Object representation of a config file where each
* key corresponds to a section or setting name and each value corresponds
* to the contents of the section or the value for a setting, respectively.
* @param {String} section - Name of the configuration section in which the
* setting resides.
* @param {String} setting - Name of the setting whose value should be
* obtained.
* @param {Boolean} [required=true] - Whether or not the setting is required
* to exist in the configuration file. If _true_ and the setting cannot be
* found, a {@link DxlError} is thrown.
* @param {Boolean} [readFromFile=false] - Whether or not to try to treat the
* value of setting as a file whose contents should be read and returned as
* the value for the setting.
* @param {String} [configPath=null] - Directory path in which the
* configuration file should reside.
* @returns {Object} Value for the setting.
* @throws {DxlError} If the setting is required but cannot be found or if
* the value should be read from a file but a problem occurs in reading
* the file - for example, if the file cannot be found.
*/
function getSetting (config, section, setting, required, readFromFile,
configPath) {
if (typeof (required) === 'undefined') { required = true }
if (typeof (configPath) === 'undefined') { configPath = null }
if (!config[section]) {
if (required) {
throw new DxlError('Required section not found in config: ' +
section)
} else {
return null
}
}
if (config[section][setting] === null || typeof (config[section][setting]) === 'undefined') {
if (required) {
throw new DxlError('Required setting not found in config: ' +
setting)
} else {
return null
}
}
var value = config[section][setting]
if (readFromFile) {
var fileToRead = util.getConfigFilePath(configPath, value)
if (fs.existsSync(fileToRead)) {
try {
value = fs.readFileSync(fileToRead)
} catch (err) {
throw new DxlError('Unable to read file for ' + setting + ': ' +
err.message)
}
} else {
throw new DxlError('Unable to locate file for ' + setting +
': ' + value)
}
}
return value
}
/**
* Parse the broker list from the config {@link Config} object
* Sample Section
* [Brokers]
* mybroker=mybroker;8883;mybroker.mcafee.com;192.168.1.12
* mybroker2=mybroker2;8883;mybroker2.mcafee.com;192.168.1.13
* ```
* @private
* @param {String} parsedConfig - Configuration settings from the configuration file
* @param {String} brokerList - List of brokers (mqtt or websocket brokers)
* @param {section} section - Section (mqtt or websocket) section to parse
* @returns {Array<Broker>} brokers An array of {@link Broker} objects
* representing brokers on the DXL fabric
* @throws {MalformedBrokerError} If one or more of the entries in the broker
* section of the configuration is invalid.
*/
function parseBrokerString (parsedConfig, brokerList, section) {
return Object.keys(brokerList).map(function (brokerKey) {
var brokerInfo = getSetting(parsedConfig, section, brokerKey, true)
if (typeof brokerInfo !== 'string') {
throw new MalformedBrokerError(
'Config broker section has incomplete entries')
}
var brokerElements = brokerInfo.split(';')
if (brokerElements.length < 2) {
throw new MalformedBrokerError(
'Missing elements in config broker line: ' + brokerInfo)
}
var portPosition = 0
try {
util.toPortNumber(brokerElements[0])
} catch (err) {
portPosition = 1
}
var id = portPosition ? brokerElements[0] : null
var port = brokerElements[portPosition]
var hosts = brokerElements.slice(portPosition + 1)
return new Broker(hosts, id, port)
})
}
/**
* The Data Exchange Layer (DXL) client configuration contains the information
* necessary to connect a {@link Client} to the DXL fabric.
*
* The configuration includes the required PKI information (client certificate,
* client private key, broker CA certificates) and the set of DXL message
* brokers that are available to connect to on the fabric.
* @example
* var dxl = require('@opendxl/dxl-client')
* var fs = require('fs')
* var config = new dxl.Config(
* fs.readFileSync('c:\\certs\\brokercerts.crt'),
* fs.readFileSync('c:\\certs\\client.crt'),
* fs.readFileSync('c:\\certs\\client.key'),
* [dxl.Broker.parse('ssl://192.168.99.100')])
*
* var client = new dxl.Client(config)
* client.connect()
* @param {String} brokerCaBundle - The bundle containing the broker CA
* certificates in PEM format.
* @param {String} cert - The client certificate in PEM format.
* @param {String} privateKey - The client private key in PEM format.
* @param {Array<Broker>} brokers - An array of {@link Broker} objects
* representing brokers comprising the DXL fabric.
* @param {Array<Broker>} webSocketBrokers - An array of {@link Broker} objects
* representing brokers on the DXL fabric supporting DXL connections over WebSockets.
* @param {Boolean} useWebSockets - If true and webSocketBrokers are defined,
* client will attempt to connect over WebSockets.
* @param {proxy} proxy Information- If non null the proxy settings will be used. This is for
* WebSocket connections only.
* @constructor
*/
function Config (brokerCaBundle, cert, privateKey, brokers, webSocketBrokers, useWebSockets, proxy) {
/**
* The bundle containing the broker CA certificates in PEM format.
* @type {String}
* @name Config#brokerCaBundle
*/
this.brokerCaBundle = brokerCaBundle
/**
* The client certificate in PEM format.
* @type {String}
* @name Config#cert
*/
this.cert = cert
/**
* The client private key in PEM format.
* @type {String}
* @name Config#privateKey
*/
this.privateKey = privateKey
/**
* An array of {@link Broker} objects representing mqtt brokers comprising the
* DXL fabric.
* @private
* @type {Array<Broker>}
* @name Config#_brokers
*/
this._brokers = brokers
/**
* An array of {@link Broker} objects representing WebSocket brokers comprising the
* DXL fabric
* @private
* @type {Array<Broker>}
* @name Config#_webSocketBrokers
*/
this._webSocketBrokers = webSocketBrokers
/**
* An array of {@link Broker} objects representing brokers comprising the
* DXL fabric. This could be webSockets brokers depending on configuration
* @type {Array<Broker>}
* @name Config#brokers
*/
this.brokers = useWebSockets ? this._webSocketBrokers : this._brokers
/**
* Flag to use websocketBrokers if defined in the config
* @type {String}
* @name Config#useWebSockets
*/
this.useWebSockets = useWebSockets
/**
* The delay between retry attempts in seconds.
* @type {Number}
* @default 1
* @name Config#reconnectDelay
*/
this.reconnectDelay = DEFAULT_RECONNECT_DELAY
/**
* The maximum period in seconds between communications with a connected
* {@link Broker}. If no other messages are being exchanged, this controls
* the rate at which the client will send ping messages to the
* {@link Broker}.
* @type {number}
* @default 1800
* @name Config#keepAliveInterval
*/
this.keepAliveInterval = DEFAULT_MQTT_KEEP_ALIVE_INTERVAL
/**
* The unique identifier of the client
* @private
* @type {String}
* @name Config#_clientId
*/
this._clientId = util.generateIdAsString()
/**
* The proxy information for the Connection via WebSockets only
* @type {Proxy}
* @name Config#_proxy
*/
this.proxy = proxy
}
/**
* This method allows creation of a {@link Config} object from a specified
* configuration file. The information contained in the file has a one-to-one
* correspondence with the {@link Config} constructor.
* ```ini
* [Certs]
* BrokerCertChain=c:\\\\certs\\\\brokercerts.crt
* CertFile=c:\\\\certs\\\\client.crt
* PrivateKey=c:\\\\certs\\\\client.key
*
* [Brokers]
* mybroker=mybroker;8883;mybroker.mcafee.com;192.168.1.12
* mybroker2=mybroker2;8883;mybroker2.mcafee.com;192.168.1.13
* ```
* @example
* var config = dxl.Config.createDxlConfigFromFile(c:\\certs\\dxlclient.config)
* @param {String} configFile - Path to the configuration file
* @returns {Config} A {@link Config} object corresponding to the specified
* configuration file.
* @throws {DxlError} If an error is encountered when attempting to read
* the configuration file.
* @throws {MalformedBrokerError} If one or more of the entries in the broker
* section of the configuration is invalid.
*/
Config.createDxlConfigFromFile = function (configFile) {
var parsedConfig = util.getConfigFileAsObject(configFile)
var configPath = path.dirname(configFile)
var brokerCaBundle = getSetting(parsedConfig, 'Certs', 'BrokerCertChain',
true, true, configPath)
var cert = getSetting(parsedConfig, 'Certs', 'CertFile', true, true,
configPath)
var privateKey = getSetting(parsedConfig, 'Certs', 'PrivateKey', true, true,
configPath)
// parse MQTT broker list
var brokers = []
if (parsedConfig.hasOwnProperty('Brokers')) {
brokers = parseBrokerString(parsedConfig, parsedConfig.Brokers, 'Brokers')
}
// parse WebSockets broker list
var webSocketBrokers = []
if (parsedConfig.hasOwnProperty('BrokersWebSockets')) {
webSocketBrokers = parseBrokerString(parsedConfig, parsedConfig.BrokersWebSockets, 'BrokersWebSockets')
}
var useWebSocketBrokers = getSetting(parsedConfig, 'General', 'UseWebSockets', false)
// read proxy settings from config file if present
var proxyAddress = getSetting(parsedConfig, 'Proxy', 'Address', false)
var proxy = null
if (proxyAddress) {
var port = util.toPortNumber(getSetting(parsedConfig, 'Proxy', 'Port', true))
var user = getSetting(parsedConfig, 'Proxy', 'User', false)
var password
if (user) {
password = getSetting(parsedConfig, 'Proxy', 'Password', true)
}
proxy = new Proxy(proxyAddress, port, user, password)
}
// check for non boolean values 'True' is non boolean
if (useWebSocketBrokers != null && typeof useWebSocketBrokers !== 'boolean') {
useWebSocketBrokers = useWebSocketBrokers.toLowerCase() === 'true'
}
// If false MQTT over tcp will be used. If only WebSocket brokers are specified this will default to true.
if (useWebSocketBrokers == null || !useWebSocketBrokers) {
useWebSocketBrokers = webSocketBrokers.length > 0 && brokers.length <= 0
}
var config = new Config(brokerCaBundle, cert, privateKey, brokers, webSocketBrokers, useWebSocketBrokers, proxy)
var clientId = getSetting(parsedConfig, 'General', 'ClientId', false)
if (clientId) {
config._clientId = clientId
}
return config
}
/**
* Provisions a DXL client by performing the following steps:
*
* * Either generates a certificate signing request and private key, storing
* each to a file (the default), or reads the certificate signing request
* from a file (if the `certRequestFile` property under the `options` object
* is present and has a truthy value).
*
* * Sends the certificate signing request to a signing endpoint on a
* management server. If the request is successfully authenticated and
* authorized, the management server is expected to respond with the following
* data:
*
* * [ca bundle] - a concatenation of one or more PEM-encoded CA
* certificates
* * [signed client cert] - a PEM-encoded certificate signed from the
* certificate request
* * [broker config] - zero or more lines, each delimited by a line feed
* character, for each of the brokers known to the management service.
* Each line contains a key and value, delimited by an equal sign. The
* key contains a broker guid. The value contains other metadata for the
* broker, e.g., the broker guid, port, hostname, and ip address. For
* example: "[guid1]=[guid1];8883;broker;10.10.1.1\n[guid2]=[guid2]...".
*
* * Saves the [ca bundle] and [signed client cert] to separate files.
* * Creates a "dxlclient.config" file with the following sections:
*
* * A "Certs" section with certificate configuration which refers to the
* locations of the private key, ca bundle, and certificate files.
* * A "Brokers" section with the content of the [broker config] provided
* by the management service.
* @param {String} configDir - Directory in which to store the configuration
* data.
* @param {String} commonOrCsrFileName - A string representing either
* a common name (CN) to add into the generated file or the path to the
* location of an existing CSR file. The parameter is interpreted as a path
* to an existing CSR file if a property named certRequestFile exists on the
* command object and has a truthy value. If the parameter represents a path
* to an existing CSR file, this function does not generate a new CSR file.
* @param {Object} hostInfo - Info for the management service host.
* @param {String} hostInfo.user - Username to run remote commands as.
* @param {String} hostInfo.password - Password for the management service user.
* @param {String} [hostInfo.port=8443] - Port at which the management service
* resides.
* @param {String} [hostInfo.truststore] - Location of a file of CA certificates
* to use when verifying the management service's certificate. If no value is
* specified, no validation of the management service's certificate is
* performed.
* @param {Object} [options] - Additional options for the provision operation.
* @param {Boolean} [options.certRequestFile] - If present and truthy,
* interprets the commonOrCsrFileName parameter as the name of an existing CSR
* file.
* @param {String} [options.filePrefix=client] - Prefix of the private key, CSR,
* and certificate to store.
* @param {String} [options.opensslbin] - Path to the openssl executable. If not
* specified, the function attempts to find the openssl executable from the
* environment path.
* @param {String} [options.passphrase] - Password to use for encrypting the
* private key.
* @param {Array<String>} [options.san] - List of subject alternative names to
* add to the CSR.
* @param {String} [options.country] - Country (C) to use in the CSR's Subject
* DN.
* @param {String} [options.stateOrProvince] - State or province (ST) to use in
* the CSR's Subject DN.
* @param {String} [options.locality] - Locality (L) to use in the CSR's Subject
* DN.
* @param {String} [options.organization] - Organization (O) to use in the CSR's
* Subject DN.
* @param {String} [options.organizationalUnit] - Organizational Unit (OU) to
* use in the CSR's Subject DN.
* @param {String} [options.emailAddress] - E-mail address to use in the CSR's
* Subject DN.
* @param {Function} [options.doneCallback] - Callback to invoke once the
* provisioned configuration has been stored. If an error occurs, the first
* parameter supplied to the `doneCallback` is an `Error` instance
* containing failure details.
*/
Config.provisionConfig = function (configDir, commonOrCsrFileName,
hostInfo, options) {
provisionConfig(configDir, commonOrCsrFileName, hostInfo, options)
}
module.exports = Config