@flowfuse/device-agent
Version:
An Edge Agent for running Node-RED instances deployed from the FlowFuse Platform
216 lines (197 loc) • 6.87 kB
JavaScript
const fs = require('fs')
const yaml = require('yaml')
const { hasProperty } = require('./utils')
/**
* @typedef ConfigOptions
* @property {string} deviceFile - The path to the device config file
* @property {string} [provisioningToken] - The provisioning token
* @property {string} [forgeURL] - The forge URL
* @property {string} [provisioningTeam] - The provisioning team
* @property {string} [deviceId] - The device ID
* @property {string} [credentialSecret] - The credential secret
* @property {string} [deviceName] - The device name
* @property {string} [deviceType] - The device type
* @property {string} [deviceVersion] - The device version
* @property {string} [deviceDescription] - The device description
* @property {string} [brokerURL] - The broker URL
* @property {string} [brokerUsername] - The broker username
* @property {string} [brokerPassword] - The broker password
* @property {object} [httpNodeAuth] - The HTTP node auth
* @property {string} [httpNodeAuth.user] - The HTTP node auth user
* @property {string} [httpNodeAuth.pass] - The HTTP node auth password
*/
const defaults = {
port: 1880,
ui: false,
uiHost: '0.0.0.0',
uiPort: 1879
}
module.exports = {
parseDeviceConfigFile,
parseDeviceConfig,
config,
defaults
}
/**
* Load and parse the device file specified
* @param {string} deviceFile - The path to the device config file
* @returns {{ deviceConfig: Object, message: string, valid: boolean }} - The parsed options and device config file data
*/
function parseDeviceConfigFile (deviceFile) {
let config
try {
config = yaml.parse(fs.readFileSync(deviceFile, 'utf8'))
} catch (err) {
return {
valid: false,
message: `Failed to parse config file: ${err.toString()}`,
deviceConfig: {}
}
}
if (!config) {
return {
valid: false,
message: 'Config file is empty',
deviceConfig: {}
}
}
const result = parseDeviceConfig(config)
if (result.deviceConfig) {
// deviceConfig may be null if the config file is empty or invalid
// so only set the deviceFile if we have a valid deviceConfig
result.deviceConfig.deviceFile = deviceFile
}
return result
}
function parseDeviceConfig (deviceConfig) {
const result = {
valid: false,
message: '',
deviceConfig: null
}
if (typeof deviceConfig === 'string') {
try {
deviceConfig = yaml.parse(deviceConfig)
} catch (err) {
result.message = `Failed to parse config file: ${err.toString()}`
return result
}
}
if (typeof deviceConfig !== 'object' || deviceConfig === null) {
result.message = 'Config file is empty'
return result
}
// default the config
const localConfig = {
provisioningMode: false,
token: '',
forgeURL: '',
deviceId: '',
credentialSecret: ''
}
// Validate localConfig
const missing = []
if (deviceConfig.provisioningToken) {
localConfig.provisioningMode = true
localConfig.token = deviceConfig.provisioningToken
delete deviceConfig.provisioningToken
;['forgeURL', 'provisioningTeam'].forEach(opt => {
if (!deviceConfig[opt]) {
missing.push(opt)
}
})
} else {
;['deviceId', 'token', 'credentialSecret', 'forgeURL'].forEach(opt => {
if (!deviceConfig[opt]) {
missing.push(opt)
}
})
if (deviceConfig.brokerURL) {
;['brokerUsername', 'brokerPassword'].forEach(opt => {
if (!deviceConfig[opt]) {
missing.push(opt)
}
})
}
}
if (deviceConfig.httpNodeAuth) {
if (!deviceConfig.httpNodeAuth.user) {
missing.push('httpNodeAuth.user')
} else if (!deviceConfig.httpNodeAuth.pass) {
missing.push('httpNodeAuth.pass')
} else {
localConfig.httpNodeAuth = {
user: deviceConfig.httpNodeAuth.user,
pass: deviceConfig.httpNodeAuth.pass
}
}
}
if (deviceConfig.localAuth) {
if (typeof deviceConfig.localAuth !== 'object') {
missing.push('localAuth.user')
missing.push('localAuth.pass')
} else {
const hasEnabledFlag = hasProperty(deviceConfig.localAuth, 'enabled')
const enabled = hasEnabledFlag ? deviceConfig.localAuth.enabled : (deviceConfig.localAuth.user || deviceConfig.localAuth.pass)
if (enabled) {
if (!deviceConfig.localAuth.user) {
missing.push('localAuth.user')
} else if (!deviceConfig.localAuth.pass) {
missing.push('localAuth.pass')
} else {
localConfig.localAuth = {
user: deviceConfig.localAuth.user,
pass: deviceConfig.localAuth.pass
}
}
} else {
localConfig.localAuth = {
enabled: false
}
}
}
}
if (missing.length > 0) {
const missingList = missing.map(v => ` - ${v}`).join('\n')
result.message = `Config file missing required options:\n${missingList}`
return result
}
// merge deviceConfig into localConfig
Object.assign(localConfig, deviceConfig)
result.valid = true
result.deviceConfig = {
...localConfig
}
return result
}
/**
* Verify configuration options, load and verify the device config file
* @param {object} options Configuration options
* @returns Loaded configuration
*/
function config (options) {
const parsedOptions = parseDeviceConfigFile(options.deviceFile)
if (!parsedOptions.valid) {
throw new Error(parsedOptions.message)
}
delete parsedOptions.config
const version = require('../package.json').version
const result = {
version,
...defaults,
...parsedOptions.deviceConfig,
...options
}
if (parsedOptions.deviceConfig.provisioningMode) {
let provisioningExtras = null
provisioningExtras = {
...parsedOptions.deviceConfig
}
const excludeProps = ['provisioningMode', 'provisioningName', 'provisioningTeam', 'provisioningToken', 'token', 'forgeURL', 'deviceId', 'credentialSecret', 'deviceFile', 'brokerURL', 'brokerUsername', 'brokerPassword', 'autoProvisioned', 'cliSetup']
for (const prop of excludeProps) {
delete provisioningExtras[prop]
}
result.provisioningExtras = provisioningExtras
}
return result
}