UNPKG

@flowfuse/device-agent

Version:

An Edge Agent for running Node-RED instances deployed from the FlowFuse Platform

239 lines (224 loc) 8.5 kB
const settings = require('./settings.json') const editorTheme = settings.editorTheme || {} const themeName = editorTheme.theme || 'forge-light' const themeSettings = settings[themeName] || {} const { existsSync, readFileSync } = require('fs') settings.editorTheme.header = settings.editorTheme.header || {} settings.editorTheme.header.title = settings.editorTheme.header.title || `Device: ${process.env.FF_DEVICE_NAME}` const authCache = {} /** * Get proxy agent for HTTP or HTTPS got instance. This should be applied to the `agent` property of the got instance options * * NOTE: This utility function is specifically designed for the GOT instances where the proxy is set based on the `url` * that the instance will use to make requests. As such, the proxy URL is determined based on the `httpEndPoint` provided * in conjunction with env vars `http_proxy`, `https_proxy` and `no_proxy`. * @param {String} url - http or https URL * @param {import('http').AgentOptions} proxyOptions - proxy options * @returns {{http: import('http-proxy-agent').HttpProxyAgent | undefined, https: import('https-proxy-agent').HttpsProxyAgent | undefined}} */ function getHTTPProxyAgent (url, proxyOptions) { const agent = {} if (url) { const _url = new URL(url) const proxyFromEnv = require('proxy-from-env') const proxyUrl = proxyFromEnv.getProxyForUrl(url) if (proxyUrl && _url.protocol === 'http:') { const HttpAgent = require('http-proxy-agent').HttpProxyAgent agent.http = new HttpAgent(proxyUrl, proxyOptions) } if (proxyUrl && _url.protocol === 'https:') { const HttpsAgent = require('https-proxy-agent').HttpsProxyAgent agent.https = new HttpsAgent(proxyUrl, proxyOptions) } } return agent } /** @type {import('got').default} */ let got let agentApplied const auth = { tokenHeader: 'x-access-token', // the header where node-red expects to find the token tokens: async function (token) { const [prefix, deviceId] = (token + '').split('_') if (prefix !== 'ffde' || !deviceId || !token) { return } // Check the local cache to see if this token has been verified in the // last 30 seconds if (authCache[token]) { if (Date.now() - authCache[token].ts < 30000) { return authCache[token].result } } if (!got) { agentApplied = false try { got = (await import('got')).default } catch (_error) { /* ignore */ } if (!got) { got = require('got').default } } if (!agentApplied && (process.env.all_proxy || process.env.https_proxy || process.env.http_proxy)) { got = got.extend({ agent: getHTTPProxyAgent(settings.flowforge.forgeURL, { timeout: 2000 }) }) } agentApplied = true try { const result = await got.get(`${settings.flowforge.forgeURL}/api/v1/devices/${deviceId}/editor/token`, { timeout: { request: 2000 }, headers: { 'x-access-token': token, 'user-agent': 'FlowFuse Device Agent Node-RED admin auth' } }) const { username, permissions } = JSON.parse(result.body) if (username && permissions) { // Cache the successful result authCache[token] = { ts: Date.now(), result: { username, permissions } } return { username, permissions } } } catch (err) { console.log('error getting new token', err) } } } if (settings.localAuth?.enabled) { auth.type = 'credentials' auth.users = [ { username: settings.localAuth.user, password: settings.localAuth.pass, permissions: '*' } ] } const runtimeSettings = { flowFile: 'flows.json', uiHost: '0.0.0.0', uiPort: settings.port, adminAuth: auth, httpAdminRoot: 'device-editor', httpNodeAuth: false, disableEditor: false, // permit editing of device flows as of FF v1.7.0 httpNodeCors: { origin: '*', methods: 'GET,PUT,POST,DELETE' }, externalModules: { autoInstall: true, palette: { allowInstall: true }, modules: { allowInstall: true } }, credentialSecret: settings.credentialSecret, flowforge: settings.flowforge, contextStorage: { default: 'memory', memory: { module: 'memory' }, persistent: { module: 'localfilesystem' } }, logging: { console: { level: 'info', metric: false, audit: false, handler: () => { const levelNames = { 10: 'fatal', 20: 'error', 30: 'warn', 40: 'info', 50: 'debug', 60: 'trace', 98: 'audit', 99: 'metric' } return (msg) => { let message = msg.msg try { if (typeof message === 'object' && message !== null && message.toString() === '[object Object]' && message.message) { message = message.message } } catch (e) { message = 'Exception trying to log: ' + message } console.log(JSON.stringify({ ts: Date.now(), level: levelNames[msg.level], type: msg.type, name: msg.name, id: msg.id, msg: message })) } } } }, nodesDir: settings.nodesDir || null, [themeName]: { ...themeSettings }, editorTheme: { ...editorTheme } } if (!settings.localAuth?.enabled) { runtimeSettings.editorTheme.login = { message: 'Access the editor through the FlowFuse platform' } if (themeSettings.projectURL) { runtimeSettings.editorTheme.login.button = { url: themeSettings.projectURL, label: 'Open FlowFuse Dashboard' } } } if (settings.flowforge.auditLogger?.bin && settings.flowforge.auditLogger?.url) { try { runtimeSettings.logging.auditLogger = { level: 'off', audit: true, handler: require(settings.flowforge.auditLogger.bin), loggingURL: settings.flowforge.auditLogger.url, token: settings.flowforge.auditLogger.token } } catch (e) { console.warn('Could not initialise device audit logging. Audit events will not be logged to the platform') } } if (settings.https) { ;['key', 'ca', 'cert'].forEach(key => { const filePath = settings.https[`${key}Path`] if (filePath && existsSync(filePath)) { settings.https[key] = readFileSync(filePath) } }) runtimeSettings.https = settings.https } if (settings.httpStatic) { runtimeSettings.httpStatic = settings.httpStatic } if (settings.flowforge.httpNodeAuth?.user && settings.flowforge.httpNodeAuth?.pass) { runtimeSettings.httpNodeAuth = { user: settings.flowforge.httpNodeAuth.user, pass: settings.flowforge.httpNodeAuth.pass } } else if (settings.flowforge.httpNodeAuth?.type === 'ff-user') { const ffAuthMiddleware = require(settings.flowforge.httpNodeAuth.bin).init({ type: 'flowforge-user', // baseURL is the url of the http endpoints. We don't know the external // ip/name of the device, so we use a placeholder. The code only needs the // pathname (ie '/') from this URL - the domain/etc is not used. baseURL: 'https://example.com/', forgeURL: settings.flowforge.forgeURL, teamID: settings.flowforge.teamID, clientID: settings.flowforge.httpNodeAuth.clientID, clientSecret: settings.flowforge.httpNodeAuth.clientSecret }) runtimeSettings.httpNodeMiddleware = ffAuthMiddleware runtimeSettings.ui = { middleware: ffAuthMiddleware } } module.exports = runtimeSettings