diff --git a/.eslintrc.js b/.eslintrc.js index 8f0c022ea7..325fe0a7f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,9 @@ module.exports = { plugins: [ 'vue', ], + globals: { + userconfig: true, + }, rules: { 'max-len': [ 'error', diff --git a/HomeUI/src/services/FluxService.js b/HomeUI/src/services/FluxService.js index bb342131fe..ae8b0f3de9 100644 --- a/HomeUI/src/services/FluxService.js +++ b/HomeUI/src/services/FluxService.js @@ -60,6 +60,13 @@ export default { getFluxVersion() { return Api().get('/flux/version'); }, + getFluxTags(zelidauthHeader) { + return Api().get('/flux/tags', { + headers: { + zelidauth: zelidauthHeader, + }, + }); + }, broadcastMessage(zelidauthHeader, message) { const data = message; const axiosConfig = { diff --git a/HomeUI/src/views/Home.vue b/HomeUI/src/views/Home.vue index bfa193589b..22426ce60f 100644 --- a/HomeUI/src/views/Home.vue +++ b/HomeUI/src/views/Home.vue @@ -10,6 +10,11 @@ :data="getNodeStatusResponse.nodeStatus" :variant="getNodeStatusResponse.class" /> + `${key.slice(0, 16)}: ${tags[key].slice(0, 16)}`).join(', '); + } + }, async getStaticIpInfo() { const response = await FluxService.getStaticIpInfo(); console.log(response); @@ -353,6 +369,9 @@ export default { this.$store.commit('flux/setPrivilege', data.data.privilage); this.$store.commit('flux/setZelid', zelidauth.zelid); localStorage.setItem('zelidauth', qs.stringify(zelidauth)); + if (data.data.privilage === 'admin') { + this.getTags(); + } this.showToast('success', data.data.message); } console.log(data); @@ -431,6 +450,9 @@ export default { this.$store.commit('flux/setPrivilege', response.data.data.privilage); this.$store.commit('flux/setZelid', zelidauth.zelid); localStorage.setItem('zelidauth', qs.stringify(zelidauth)); + if (response.data.data.privilage === 'admin') { + this.getTags(); + } this.showToast('success', response.data.data.message); } else { this.showToast(this.getVariant(response.data.status), response.data.data.message || response.data.data); @@ -469,6 +491,9 @@ export default { this.$store.commit('flux/setPrivilege', response.data.data.privilage); this.$store.commit('flux/setZelid', zelidauth.zelid); localStorage.setItem('zelidauth', qs.stringify(zelidauth)); + if (response.data.data.privilage === 'admin') { + this.getTags(); + } this.showToast('success', response.data.data.message); } else { this.showToast(this.getVariant(response.data.status), response.data.data.message || response.data.data); @@ -551,6 +576,9 @@ export default { this.$store.commit('flux/setPrivilege', response.data.data.privilage); this.$store.commit('flux/setZelid', zelidauth.zelid); localStorage.setItem('zelidauth', qs.stringify(zelidauth)); + if (response.data.data.privilage === 'admin') { + this.getTags(); + } this.showToast('success', response.data.data.message); } else { this.showToast(this.getVariant(response.data.status), response.data.data.message || response.data.data); @@ -602,6 +630,9 @@ export default { this.$store.commit('flux/setPrivilege', response.data.data.privilage); this.$store.commit('flux/setZelid', zelidauth.zelid); localStorage.setItem('zelidauth', qs.stringify(zelidauth)); + if (response.data.data.privilage === 'admin') { + this.getTags(); + } this.showToast('success', response.data.data.message); } else { this.showToast(this.getVariant(response.data.status), response.data.data.message || response.data.data); diff --git a/ZelBack/config/default.js b/ZelBack/config/default.js index f17c7830a7..7120a188c4 100644 --- a/ZelBack/config/default.js +++ b/ZelBack/config/default.js @@ -1,7 +1,6 @@ -// eslint-disable-next-line prefer-const -let userconfig = require('../../config/userconfig'); +let { isDevelopment } = require('../../config/userconfig'); -const isDevelopment = userconfig.initial.development || false; +isDevelopment = isDevelopment || false; module.exports = { development: isDevelopment, diff --git a/ZelBack/src/lib/log.js b/ZelBack/src/lib/log.js index 5f36ea5bac..e3972bd42e 100644 --- a/ZelBack/src/lib/log.js +++ b/ZelBack/src/lib/log.js @@ -12,7 +12,7 @@ const levels = { const logLevel = config && config.logLevel ? config.logLevel : levels.debug; -const homeDirPath = path.join(__dirname, '../../../'); +const appRootPath = path.join(__dirname, '../../../'); const fileSizeCache = {}; @@ -69,7 +69,7 @@ function debug(args) { try { console.log(args); // write to file - const filepath = `${homeDirPath}debug.log`; + const filepath = `${appRootPath}debug.log`; writeToFile(filepath, args); } catch (err) { console.error('This should not have happened'); @@ -83,7 +83,7 @@ function error(args) { } try { // write to file - const filepath = `${homeDirPath}error.log`; + const filepath = `${appRootPath}error.log`; writeToFile(filepath, args); debug(args); } catch (err) { @@ -98,7 +98,7 @@ function warn(args) { } try { // write to file - const filepath = `${homeDirPath}warn.log`; + const filepath = `${appRootPath}warn.log`; writeToFile(filepath, args); debug(args); } catch (err) { @@ -113,7 +113,7 @@ function info(args) { } try { // write to file - const filepath = `${homeDirPath}info.log`; + const filepath = `${appRootPath}info.log`; writeToFile(filepath, args); debug(args); } catch (err) { diff --git a/ZelBack/src/routes.js b/ZelBack/src/routes.js index 55f8cb511d..2ab574570f 100644 --- a/ZelBack/src/routes.js +++ b/ZelBack/src/routes.js @@ -235,6 +235,9 @@ module.exports = (app, expressWs) => { app.get('/flux/nodejsversions', cache('30 seconds'), (req, res) => { fluxService.getNodeJsVersions(req, res); }); + app.get('/flux/tags', cache('30 seconds'), (req, res) => { + fluxService.getFluxTags(req, res); + }); app.get('/flux/ip', cache('30 seconds'), (req, res) => { fluxService.getFluxIP(req, res); }); diff --git a/ZelBack/src/services/appsService.js b/ZelBack/src/services/appsService.js index 758d4733d8..393ee7cf42 100644 --- a/ZelBack/src/services/appsService.js +++ b/ZelBack/src/services/appsService.js @@ -1,4 +1,3 @@ -/* global userconfig */ const config = require('config'); const https = require('https'); const axios = require('axios'); @@ -5826,17 +5825,17 @@ async function restoreFluxPortsSupport() { try { const isUPNP = upnpService.isUPNP(); - const apiPort = userconfig.initial.apiport || config.server.apiport; - const homePort = +apiPort - 1; - const apiPortSSL = +apiPort + 1; - const syncthingPort = +apiPort + 2; + const { apiPort } = userconfig.computed; + const { homePort } = userconfig.computed; + const { apiPortSsl } = userconfig.computed; + const { syncthingPort } = userconfig.computed; const firewallActive = await fluxNetworkHelper.isFirewallActive(); if (firewallActive) { // setup UFW if active await fluxNetworkHelper.allowPort(serviceHelper.ensureNumber(apiPort)); await fluxNetworkHelper.allowPort(serviceHelper.ensureNumber(homePort)); - await fluxNetworkHelper.allowPort(serviceHelper.ensureNumber(apiPortSSL)); + await fluxNetworkHelper.allowPort(serviceHelper.ensureNumber(apiPortSsl)); await fluxNetworkHelper.allowPort(serviceHelper.ensureNumber(syncthingPort)); } diff --git a/ZelBack/src/services/benchmarkService.js b/ZelBack/src/services/benchmarkService.js index 1423831101..ad0f6fd534 100644 --- a/ZelBack/src/services/benchmarkService.js +++ b/ZelBack/src/services/benchmarkService.js @@ -1,8 +1,5 @@ -/* global userconfig */ const benchmarkrpc = require('daemonrpc'); const config = require('config'); -const path = require('path'); -const fs = require('fs'); const serviceHelper = require('./serviceHelper'); const messageHelper = require('./messageHelper'); const verificationHelper = require('./verificationHelper'); @@ -14,9 +11,6 @@ const isTestnet = userconfig.initial.testnet; const rpcport = isTestnet === true ? config.benchmark.rpcporttestnet : config.benchmark.rpcport; -const homeDirPath = path.join(__dirname, '../../../../'); -const newBenchmarkPath = path.join(homeDirPath, '.fluxbenchmark'); - let response = messageHelper.createErrorMessage(); /** @@ -32,7 +26,7 @@ async function executeCall(rpc, params) { try { let rpcuser = 'zelbenchuser'; let rpcpassword = 'zelbenchpassword'; - if (fs.existsSync(newBenchmarkPath)) { + if (userconfig.computed.isNewBenchPath) { rpcuser = 'fluxbenchuser'; rpcpassword = 'fluxbenchpassword'; } @@ -246,8 +240,8 @@ async function executeUpnpBench() { log.info('executeUpnpBench - Flux not yet synced'); return; } - const isUPNP = upnpService.isUPNP(); - if ((userconfig.initial.apiport && userconfig.initial.apiport !== config.server.apiport) || isUPNP) { + + if (upnpService.isUPNP()) { log.info('Calling FluxBench startMultiPortBench'); log.info(await startMultiPortBench()); } diff --git a/ZelBack/src/services/daemonService/daemonServiceMiscRpcs.js b/ZelBack/src/services/daemonService/daemonServiceMiscRpcs.js index 7a99123807..909bdc3423 100644 --- a/ZelBack/src/services/daemonService/daemonServiceMiscRpcs.js +++ b/ZelBack/src/services/daemonService/daemonServiceMiscRpcs.js @@ -1,4 +1,3 @@ -/* global userconfig */ const messageHelper = require('../messageHelper'); const daemonServiceUtils = require('./daemonServiceUtils'); const daemonServiceBlockchainRpcs = require('./daemonServiceBlockchainRpcs'); diff --git a/ZelBack/src/services/dockerService.js b/ZelBack/src/services/dockerService.js index e5b2895be2..adfac98d6c 100644 --- a/ZelBack/src/services/dockerService.js +++ b/ZelBack/src/services/dockerService.js @@ -77,7 +77,7 @@ function getAppDockerNameIdentifier(appName) { Labels?: { [label: string]: string } | undefined; abortSignal?: AbortSignal; - * @returns {object} Network + * @returns {Promise} Network */ async function dockerCreateNetwork(options) { const network = await docker.createNetwork(options); @@ -89,7 +89,7 @@ async function dockerCreateNetwork(options) { * * @param {object} netw - Network object * - * @returns {Buffer} + * @returns {Promise} */ async function dockerRemoveNetwork(netw) { const network = await netw.remove(); @@ -101,7 +101,7 @@ async function dockerRemoveNetwork(netw) { * * @param {object} netw - Network object * - * @returns {object} ispect network object + * @returns {Promise} ispect network object */ async function dockerNetworkInspect(netw) { const network = await netw.inspect(); @@ -116,7 +116,7 @@ async function dockerNetworkInspect(netw) { * @param {bool} [size] - Return the size of container as fields SizeRw and SizeRootFs. * @param {string} [filter] Filters to process on the container list, encoded as JSON - * @returns {array} containers list + * @returns {Promise} containers list */ async function dockerListContainers(all, limit, size, filter) { const options = { @@ -132,7 +132,7 @@ async function dockerListContainers(all, limit, size, filter) { /** * Returns a list of images on the server. * - * @returns {array} images list + * @returns {Promise} images list */ async function dockerListImages() { const containers = await docker.listImages(); @@ -142,7 +142,7 @@ async function dockerListImages() { /** * Returns a docker container found by name or ID * @param {string} idOrName - * @returns {object} dockerContainer from list containers + * @returns {Promise} dockerContainer from list containers */ async function getDockerContainerOnly(idOrName) { const containers = await dockerListContainers(true); @@ -157,7 +157,7 @@ async function getDockerContainerOnly(idOrName) { * Returns a docker container found by name or ID * * @param {string} idOrName - * @returns {object} dockerContainer + * @returns {Promise} dockerContainer */ async function getDockerContainerByIdOrName(idOrName) { const myContainer = await getDockerContainerOnly(idOrName); @@ -168,7 +168,7 @@ async function getDockerContainerByIdOrName(idOrName) { * Returns low-level information about a container. * * @param {string} idOrName - * @returns {object} + * @returns {Promise} */ async function dockerContainerInspect(idOrName) { // container ID or name @@ -388,7 +388,7 @@ async function dockerContainerLogsStream(idOrName, res, callback) { * @param {string} idOrName * @param {number} lines * - * @returns {buffer} + * @returns {Promise} */ async function dockerContainerLogs(idOrName, lines) { // container ID or name @@ -437,7 +437,7 @@ async function obtainPayloadFromStorage(url, appName) { * @param {object} appSpecifications * @param {string} appName * @param {bool} isComponent - * @returns {object} + * @returns {Promise} */ async function appDockerCreate(appSpecifications, appName, isComponent, fullAppSpecs) { const identifier = isComponent ? `${appSpecifications.name}_${appName}` : appName; @@ -668,7 +668,7 @@ async function appDockerCreate(appSpecifications, appName, isComponent, fullAppS * Starts app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerStart(idOrName) { // container ID or name @@ -682,7 +682,7 @@ async function appDockerStart(idOrName) { * Stops app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerStop(idOrName) { // container ID or name @@ -696,7 +696,7 @@ async function appDockerStop(idOrName) { * Restarts app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Prmoise} message */ async function appDockerRestart(idOrName) { // container ID or name @@ -710,7 +710,7 @@ async function appDockerRestart(idOrName) { * Kills app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerKill(idOrName) { // container ID or name @@ -724,7 +724,7 @@ async function appDockerKill(idOrName) { * Removes app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Prmise} message */ async function appDockerRemove(idOrName) { // container ID or name @@ -738,7 +738,7 @@ async function appDockerRemove(idOrName) { * Removes app's docker image. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerImageRemove(idOrName) { // container ID or name @@ -751,7 +751,7 @@ async function appDockerImageRemove(idOrName) { * Pauses app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerPause(idOrName) { // container ID or name @@ -765,7 +765,7 @@ async function appDockerPause(idOrName) { * Unpauses app's docker. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerUnpause(idOrName) { // container ID or name @@ -779,7 +779,7 @@ async function appDockerUnpause(idOrName) { * Returns app's docker's active processes. * * @param {string} idOrName - * @returns {string} message + * @returns {Promise} message */ async function appDockerTop(idOrName) { // container ID or name @@ -792,7 +792,7 @@ async function appDockerTop(idOrName) { /** * Creates flux docker network if doesn't exist * OBSOLETE - * @returns {object} response + * @returns {Promise} response */ async function createFluxDockerNetwork() { // check if fluxDockerNetwork exists @@ -823,7 +823,7 @@ async function createFluxDockerNetwork() { /** * Creates flux application docker network if doesn't exist * - * @returns {object} response + * @returns {Promise} response */ async function createFluxAppDockerNetwork(appname, number) { // check if fluxDockerNetwork of an appexists @@ -854,7 +854,7 @@ async function createFluxAppDockerNetwork(appname, number) { /** * Removes flux application docker network if exists * - * @returns {object} response + * @returns {Promise} response */ async function removeFluxAppDockerNetwork(appname) { // check if fluxDockerNetwork of an app exists @@ -905,7 +905,7 @@ async function pruneImages() { /** * Return docker system information * - * @returns {object} + * @returns {Promise} */ async function dockerInfo() { const info = await docker.info(); @@ -915,7 +915,7 @@ async function dockerInfo() { /** * Returns the version of Docker that is running and various information about the system that Docker is running on. * - * @returns {object} + * @returns {Promise} */ async function dockerVersion() { const version = await docker.version(); @@ -925,7 +925,7 @@ async function dockerVersion() { /** * Returns docker events * - * @returns {object} + * @returns {Promise} */ async function dockerGetEvents() { const events = await docker.getEvents(); @@ -935,7 +935,7 @@ async function dockerGetEvents() { /** * Returns docker usage information * - * @returns {object} + * @returns {Promise} */ async function dockerGetUsage() { const df = await docker.df(); diff --git a/ZelBack/src/services/fluxNetworkHelper.js b/ZelBack/src/services/fluxNetworkHelper.js index ff3cd383a5..aad0794009 100644 --- a/ZelBack/src/services/fluxNetworkHelper.js +++ b/ZelBack/src/services/fluxNetworkHelper.js @@ -1,12 +1,9 @@ -/* global userconfig */ -/* eslint-disable no-underscore-dangle */ const config = require('config'); const zeltrezjs = require('zeltrezjs'); const nodecmd = require('node-cmd'); const fs = require('fs').promises; const path = require('path'); const os = require('os'); -// eslint-disable-next-line import/no-extraneous-dependencies const util = require('util'); const { LRUCache } = require('lru-cache'); const log = require('../lib/log'); @@ -449,7 +446,7 @@ async function getRandomConnection() { } const randomNode = Math.floor((Math.random() * zlLength)); // we do not really need a 'random' const ip = nodeList[randomNode].ip || nodeList[randomNode].ipaddress; - const apiPort = userconfig.initial.apiport || config.server.apiport; + const { apiPort } = userconfig.computed; if (!ip || !myFluxIP || ip === userconfig.initial.ipaddress || ip === myFluxIP || ip === `${userconfig.initial.ipaddress}:${apiPort}` || ip.split(':')[0] === myFluxIP.split(':')[0]) { return null; @@ -737,7 +734,7 @@ async function checkMyFluxAvailability(retryNumber = 0) { const axiosConfigAux = { timeout: 7000, }; - const apiPort = userconfig.initial.apiport || config.server.apiport; + const { apiPort } = userconfig.computed; const resMyAvailability = await serviceHelper.axiosGet(`http://${askingIP}:${askingIpPort}/flux/checkfluxavailability?ip=${myIP}&port=${apiPort}`, axiosConfigAux).catch((error) => { log.error(`${askingIP} is not reachable`); log.error(error); @@ -854,7 +851,8 @@ async function adjustExternalIP(ip) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -867,8 +865,8 @@ async function adjustExternalIP(ip) { if (oldUserConfigIp && v4exact.test(oldUserConfigIp) && !myCache.has(ip)) { myCache.set(ip, ip); - const newIP = userconfig.initial.apiport !== 16127 ? `${ip}:${userconfig.initial.apiport}` : ip; - const oldIP = userconfig.initial.apiport !== 16127 ? `${oldUserConfigIp}:${userconfig.initial.apiport}` : oldUserConfigIp; + const newIP = userconfig.computed.apiPort !== 16127 ? `${ip}:${userconfig.computed.apiPort}` : ip; + const oldIP = userconfig.computed.apiPort !== 16127 ? `${oldUserConfigIp}:${userconfig.computed.apiPort}` : oldUserConfigIp; log.info(`New public Ip detected: ${newIP}, old Ip:${oldIP} , updating the FluxNode info in the network`); // eslint-disable-next-line global-require const appsService = require('./appsService'); @@ -1244,7 +1242,7 @@ async function allowPortApi(req, res) { /** * To check if a firewall is active. - * @returns {boolean} True if a firewall is active. Otherwise false. + * @returns {Promise} True if a firewall is active. Otherwise false. */ async function isFirewallActive() { try { @@ -1268,11 +1266,11 @@ async function isFirewallActive() { async function adjustFirewall() { try { const cmdAsync = util.promisify(nodecmd.get); - const apiPort = userconfig.initial.apiport || config.server.apiport; - const homePort = +apiPort - 1; - const apiSSLPort = +apiPort + 1; - const syncthingPort = +apiPort + 2; - let ports = [apiPort, homePort, apiSSLPort, syncthingPort, 80, 443, 16125]; + const { apiPort } = userconfig.computed; + const { homePort } = userconfig.computed; + const { apiPortSsl } = userconfig.computed; + const { syncthingPort } = userconfig.computed; + let ports = [apiPort, homePort, apiPortSsl, syncthingPort, 80, 443, 16125]; const fluxCommunicationPorts = config.server.allowedPorts; ports = ports.concat(fluxCommunicationPorts); const firewallActive = await isFirewallActive(); diff --git a/ZelBack/src/services/fluxService.js b/ZelBack/src/services/fluxService.js index 229dc16775..f1ef78902f 100644 --- a/ZelBack/src/services/fluxService.js +++ b/ZelBack/src/services/fluxService.js @@ -1,4 +1,3 @@ -/* global userconfig */ const nodecmd = require('node-cmd'); const path = require('path'); const config = require('config'); @@ -431,6 +430,23 @@ function getNodeJsVersions(req, res) { return res ? res.json(message) : message; } +/** + * To get node operator specified tags. Allows an operator to tag nodes, + * and only an operator can view them. + * @param {object} req Request. + * @param {object} res Response. + * @returns + */ +async function getFluxTags(req, res) { + const authorized = await verificationHelper.verifyPrivilege('admin', req); + + const message = authorized + ? messageHelper.createDataMessage(userconfig.computed.tags) + : messageHelper.errUnauthorizedMessage(); + + return res ? res.json(message) : message; +} + /** * To show FluxOS IP address. * @param {object} req Request. @@ -553,8 +569,8 @@ function getBlockedPorts(req, res) { * @returns {object} Message. */ function getAPIPort(req, res) { - const routerIP = userconfig.initial.apiport || '16127'; - const message = messageHelper.createDataMessage(routerIP); + const { apiPort } = userconfig.computed; + const message = messageHelper.createDataMessage(apiPort); return res ? res.json(message) : message; } @@ -618,13 +634,8 @@ async function benchmarkDebug(req, res) { const errMessage = messageHelper.errUnauthorizedMessage(); return res.json(errMessage); } - const homeDirPath = path.join(__dirname, '../../../../'); - const newBenchmarkPath = path.join(homeDirPath, '.fluxbenchmark'); - let datadir = `${homeDirPath}.zelbenchmark`; - if (fs.existsSync(newBenchmarkPath)) { - datadir = newBenchmarkPath; - } - const filepath = `${datadir}/debug.log`; + + const filepath = path.join(userconfig.computed.benchmarkPath, 'debug.log'); return res.download(filepath, 'debug.log'); } @@ -664,13 +675,7 @@ async function tailDaemonDebug(req, res) { async function tailBenchmarkDebug(req, res) { const authorized = await verificationHelper.verifyPrivilege('adminandfluxteam', req); if (authorized === true) { - const homeDirPath = path.join(__dirname, '../../../../'); - const newBenchmarkPath = path.join(homeDirPath, '.fluxbenchmark'); - let datadir = `${homeDirPath}.zelbenchmark`; - if (fs.existsSync(newBenchmarkPath)) { - datadir = newBenchmarkPath; - } - const filepath = `${datadir}/debug.log`; + const filepath = path.join(userconfig.computed.benchmarkPath, 'debug.log'); const exec = `tail -n 100 ${filepath}`; nodecmd.get(exec, (err, data) => { if (err) { @@ -694,8 +699,7 @@ async function tailBenchmarkDebug(req, res) { * @returns {object} FluxOS .log file. */ async function fluxLog(res, filelog) { - const homeDirPath = path.join(__dirname, '../../../'); - const filepath = `${homeDirPath}${filelog}.log`; + const filepath = path.join(userconfig.computed.appRootPath, `${filelog}.log`); return res.download(filepath, `${filelog}.log`); } @@ -789,8 +793,7 @@ async function fluxDebugLog(req, res) { async function tailFluxLog(req, res, logfile) { const authorized = await verificationHelper.verifyPrivilege('adminandfluxteam', req); if (authorized === true) { - const homeDirPath = path.join(__dirname, '../../../'); - const filepath = `${homeDirPath}${logfile}.log`; + const filepath = path.join(userconfig.computed.appRootPath, `${logfile}.log`); const exec = `tail -n 100 ${filepath}`; nodecmd.get(exec, (err, data) => { if (err) { @@ -1069,7 +1072,8 @@ async function adjustCruxID(req, res) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1125,7 +1129,8 @@ async function adjustKadenaAccount(req, res) { kadena: '${kadenaURI}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1168,7 +1173,8 @@ async function adjustRouterIP(req, res) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${routerip}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1223,7 +1229,8 @@ async function adjustBlockedPorts(req, res) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1286,7 +1293,8 @@ async function adjustAPIPort(req, res) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(+apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: ${apiport}, routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1348,7 +1356,8 @@ async function adjustBlockedRepositories(req, res) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${userconfig.initial.pgpPrivateKey || ''}\`, pgpPublicKey: \`${userconfig.initial.pgpPublicKey || ''}\`, @@ -1473,6 +1482,7 @@ module.exports = { reindexDaemon, getFluxVersion, getNodeJsVersions, + getFluxTags, getFluxIP, getFluxZelID, getFluxPGPidentity, diff --git a/ZelBack/src/services/fluxportControllerService.js b/ZelBack/src/services/fluxportControllerService.js new file mode 100644 index 0000000000..6f32b94043 --- /dev/null +++ b/ZelBack/src/services/fluxportControllerService.js @@ -0,0 +1,133 @@ +const path = require('node:path'); +const { writeFile, readFile } = require('node:fs/promises'); + +const log = require('../lib/log'); +const generalService = require('./generalService'); +const upnpService = require('./upnpService'); + +const { FluxGossipServer, logController: fpcLogController } = require('@megachips/fluxport-controller'); +const { executeCall: executeBenchmarkCall } = require('./benchmarkService'); + +const GOSSIPSERVER_TIMEOUT = 90; + +let apiPort = null; +let routerIp = null; +let outPoint = null; +let gossipServer = null; + +async function getApiPort() { + return new Promise((resolve, reject) => { + if (apiPort) resolve(apiPort); + + if (!(gossipServer)) reject(new Error('gossipServer not ready')); + + const timeout = setTimeout(() => reject(new Error('Timeout waiting for port')), GOSSIPSERVER_TIMEOUT * 1000); + + gossipServer.once('portConfirmed', (port) => { + clearTimeout(timeout); + resolve(port); + }); + }); +} + +function getRouterIp() { + return new Promise((resolve, reject) => { + if (routerIp) resolve(routerIp); + + if (!(gossipServer)) reject(new Error('gossipServer not ready')); + + const timeout = setTimeout(() => reject(new Error('Timeout waiting for ip')), GOSSIPSERVER_TIMEOUT * 1000); + + gossipServer.once('routerIpConfirmed', (ip) => { + clearTimeout(timeout); + resolve(ip); + }); + }); +} + +function stopGossipServer() { + gossipServer.stop(); + gossipServer = null; +} + +async function startGossipServer() { + if (gossipServer) return gossipServer; + + log.info('Starting GossipServer'); + + const logPath = path.join(userconfig.computed.appRootPath, 'debug.log'); + fpcLogController.addLoggerTransport('file', { logLevel: 'info', filePath: logPath }); + + try { + // this is reliant on fluxd running + const res = await generalService.obtainNodeCollateralInformation(); + // const res = { txhash: "txtest", txindex: 0 } + outPoint = { txhash: res.txhash, outidx: res.txindex }; + } catch { + log.error('Error getting collateral info from daemon.'); + return null; + } + + if (!(await upnpService.ufwAllowSsdpforInit())) { + log.error('Error adjusting firewallfor SSDP.'); + return null; + } + + // Using the port 16197 is fine here, even if in use. Flux uses TCP + // whereas this uses UDP. Also, the gossipServer doesn't bind to the interface + // address, only the multicast address so you won't get EADDRINUSE. Its good as + // Flux already opens this port. + gossipServer = new FluxGossipServer(outPoint, { + // seems as good as any multicast address + multicastGroup: '239.19.38.57', + port: 16197, + }); + + gossipServer.on('portConfirmed', async (port) => { + if (port && port !== apiPort) { + log.info(`Gossip server got new apiPort: ${port}, updating`); + // would be great if bench exposed an api for the apiport, this is brutal + // or just tried all 8 ports on localhost, until it found one. + const { benchmarkConfigFilePath } = userconfig.computed; + const priorFile = await readFile(benchmarkConfigFilePath, { flag: 'a+' }); + if (priorFile !== `fluxport=${port}`) { + await writeFile(benchmarkConfigFilePath, `fluxport=${port}`); + await executeBenchmarkCall('restartnodebenchmarks'); + } + apiPort = port; + } + }); + + gossipServer.on('routerIpConfirmed', async (ip) => { + if (ip && ip !== routerIp) { + log.info(`Gossip server got new routerIp: ${ip}, updating`); + await upnpService.ufwRemoveAllowSsdpforInit(); + // removed this. Regarding control plane / data plane. If Flux + // control plane goes down, it shouldn't interfere with the data plane. + // This was more aimed at if we have mappings that don't belong to this node. + // Maybe look up the descriptions? or just let them time out... + // await upnpService.cleanOldMappings(ip); + routerIp = ip; + } + }); + + gossipServer.on('startError', () => { + log.error('Upnp error starting gossipserver, starting again in 2 minutes...'); + setTimeout(() => gossipServer.start(), 2 * 60 * 1000); + }); + + gossipServer.start(); + return gossipServer; +} + +function getGossipServer() { + return gossipServer; +} + +module.exports = { + getApiPort, + getRouterIp, + startGossipServer, + stopGossipServer, + getGossipServer, +}; diff --git a/ZelBack/src/services/generalService.js b/ZelBack/src/services/generalService.js index bafcb9b901..bc60c894ac 100644 --- a/ZelBack/src/services/generalService.js +++ b/ZelBack/src/services/generalService.js @@ -34,7 +34,7 @@ function getCollateralInfo(collateralOutpoint) { /** * To return a transaction hash and index of our node collateral - * @returns {object} Collateral info object. + * @returns {Promise} Collateral info object. * @property {string} txhash Transaction hash. * @property {number} txindex Transaction index. */ @@ -45,8 +45,7 @@ async function obtainNodeCollateralInformation() { if (nodeStatus.status === 'error') { throw nodeStatus.data; } - const collateralInformation = getCollateralInfo(nodeStatus.data.collateral); - return collateralInformation; + return getCollateralInfo(nodeStatus.data.collateral); } /** @@ -178,7 +177,7 @@ async function isNodeStatusConfirmed() { /** * Checks if a node's FluxOS database is synced with the node's daemon database. - * @returns {boolean} True if FluxOS databse height is within 1 of the daemon database height. False if not within 1 of the height or if there is an error. + * @returns {Promise} True if FluxOS databse height is within 1 of the daemon database height. False if not within 1 of the height or if there is an error. */ async function checkSynced() { try { diff --git a/ZelBack/src/services/idService.js b/ZelBack/src/services/idService.js index 83deb7dc40..11fcc8c1f2 100644 --- a/ZelBack/src/services/idService.js +++ b/ZelBack/src/services/idService.js @@ -1,4 +1,3 @@ -/* global userconfig */ const config = require('config'); const qs = require('qs'); const os = require('os'); diff --git a/ZelBack/src/services/pgpService.js b/ZelBack/src/services/pgpService.js index 392439eb4c..3da0e62094 100644 --- a/ZelBack/src/services/pgpService.js +++ b/ZelBack/src/services/pgpService.js @@ -1,4 +1,3 @@ -/* global userconfig */ const config = require('config'); const path = require('path'); const fs = require('fs').promises; @@ -26,7 +25,8 @@ async function adjustPGPidentity(privateKey, publicKey) { kadena: '${userconfig.initial.kadena || ''}', testnet: ${userconfig.initial.testnet || false}, development: ${userconfig.initial.development || false}, - apiport: ${Number(userconfig.initial.apiport || config.server.apiport)}, + upnp: ${userconfig.initial.upnp || false}, + apiport: '${userconfig.initial.apiport || ''}', routerIP: '${userconfig.initial.routerIP || ''}', pgpPrivateKey: \`${privateKey}\`, pgpPublicKey: \`${publicKey}\`, diff --git a/ZelBack/src/services/serviceManager.js b/ZelBack/src/services/serviceManager.js index c9c6fefef7..d214fe9255 100644 --- a/ZelBack/src/services/serviceManager.js +++ b/ZelBack/src/services/serviceManager.js @@ -1,4 +1,3 @@ -/* global userconfig */ const config = require('config'); const log = require('../lib/log'); @@ -15,13 +14,13 @@ const upnpService = require('./upnpService'); const syncthingService = require('./syncthingService'); const pgpService = require('./pgpService'); -const apiPort = userconfig.initial.apiport || config.server.apiport; const development = userconfig.initial.development || false; /** * To start FluxOS. A series of checks are performed on port and UPnP (Universal Plug and Play) support and mapping. Database connections are established. The other relevant functions required to start FluxOS services are called. */ async function startFluxFunctions() { + const { apiPort } = userconfig.computed; try { if (!config.server.allowedPorts.includes(+apiPort)) { log.error(`Flux port ${apiPort} is not supported. Shutting down.`); @@ -29,7 +28,7 @@ async function startFluxFunctions() { } // User configured UPnP node with routerIP, UPnP has already been verified and setup - if (userconfig.initial.routerIP) { + if (userconfig.initial.upnp || userconfig.initial.routerIP) { setInterval(() => { upnpService.adjustFirewallForUPNP(); }, 1 * 60 * 60 * 1000); // every 1 hours diff --git a/ZelBack/src/services/syncthingService.js b/ZelBack/src/services/syncthingService.js index d8ee49f879..01c9175737 100644 --- a/ZelBack/src/services/syncthingService.js +++ b/ZelBack/src/services/syncthingService.js @@ -1,4 +1,3 @@ -/* global userconfig */ const config = require('config'); const nodecmd = require('node-cmd'); const axios = require('axios'); @@ -2123,8 +2122,7 @@ async function adjustSyncthing() { } const currentConfigOptions = await getConfigOptions(); const currentDefaultsFolderOptions = await getConfigDefaultsFolder(); - const apiPort = userconfig.initial.apiport || config.server.apiport; - const myPort = +apiPort + 2; // end with 9 eg 16139 + const myPort = userconfig.computed.syncthingPort; // end with 9 eg 16139 // adjust configuration const newConfig = { globalAnnounceEnabled: false, diff --git a/ZelBack/src/services/upnpService.js b/ZelBack/src/services/upnpService.js index 5bf9d56657..73e2307791 100644 --- a/ZelBack/src/services/upnpService.js +++ b/ZelBack/src/services/upnpService.js @@ -1,16 +1,13 @@ -/* global userconfig */ -const config = require('config'); -const natUpnp = require('@runonflux/nat-upnp'); +const natUpnp = require('@megachips/nat-upnp'); const serviceHelper = require('./serviceHelper'); const messageHelper = require('./messageHelper'); const verificationHelper = require('./verificationHelper'); const nodecmd = require('node-cmd'); -// eslint-disable-next-line import/no-extraneous-dependencies const util = require('util'); const log = require('../lib/log'); -const client = new natUpnp.Client(); +const client = new natUpnp.Client({ cacheGateway: true }); let upnpMachine = false; @@ -43,27 +40,73 @@ async function isFirewallActive() { } /** - * To adjust a firewall to allow comms between host and router. + * To allow inbound unicast SSDP M-SEARCH query response from as yet undiscovered + * router. I.e. allow SSDP for any lan address. This gets removed and updated to router + * Address once router found. Only applies to nodes using auto UPnP configuration. + * @returns {Promise} True if SSDP is allowed. Otherwise false. + */ +async function ufwAllowSsdpforInit() { + if (!(await isFirewallActive())) return true; + + const cmdAsync = util.promisify(nodecmd.get); + // allow from any address as we are looking for a router IP. + const allowSsdpCmd = 'sudo ufw allow from any port 1900 to any proto udp > /dev/null 2>&1'; + try { + await cmdAsync(allowSsdpCmd); + return true; + } catch (error) { + log.error(error); + return false; + } +} + +/** + * To remove allow inbound unicast SSDP M-SEARCH query response from LAN. + * @returns {Promise} True if SSDP removed / not exist. Otherwise false. + */ +async function ufwRemoveAllowSsdpforInit() { + if (!(await isFirewallActive())) return true; + + const cmdAsync = util.promisify(nodecmd.get); + // allow from any address as are looking for a router IP. + const removeAllowSsdpCmd = 'sudo ufw delete allow from any port 1900 to any proto udp > /dev/null 2>&1'; + try { + await cmdAsync(removeAllowSsdpCmd); + return true; + } catch (error) { + // above rule returns 0 for non existent rule so this shouldn't fire unless actual error + log.error(error); + return false; + } +} + +/** + * * To adjust a firewall to allow comms between host and router. */ async function adjustFirewallForUPNP() { + const { routerIp } = userconfig.computed; + try { - let { routerIP } = userconfig.initial; - routerIP = serviceHelper.ensureString(routerIP); - if (routerIP) { + if (routerIp) { const cmdAsync = util.promisify(nodecmd.get); const firewallActive = await isFirewallActive(); if (firewallActive) { + // why allow outbound?!? There is a default allow const execA = 'sudo ufw allow out from any to 239.255.255.250 port 1900 proto udp > /dev/null 2>&1'; - const execB = `sudo ufw allow from ${routerIP} port 1900 to any proto udp > /dev/null 2>&1`; - const execC = `sudo ufw allow out from any to ${routerIP} proto tcp > /dev/null 2>&1`; - const execD = `sudo ufw allow from ${routerIP} to any proto udp > /dev/null 2>&1`; + // this is superfulous as there is an allow for allow udp below + const execB = `sudo ufw allow from ${routerIp} port 1900 to any proto udp > /dev/null 2>&1`; + const execC = `sudo ufw allow out from any to ${routerIp} proto tcp > /dev/null 2>&1`; + const execD = `sudo ufw allow from ${routerIp} to any proto udp > /dev/null 2>&1`; + // added this as we are now using multicast and need to be able to receive igmp queries + const execE = 'sudo ufw allow to any proto igmp > /dev/null 2>&1'; await cmdAsync(execA); await cmdAsync(execB); await cmdAsync(execC); await cmdAsync(execD); + await cmdAsync(execE); log.info('Firewall adjusted for UPNP'); } else { - log.info('RouterIP is set but firewall is not active. Adjusting not applied for UPNP'); + log.info(`Router IP: ${routerIp} set but firewall is not active. Adjustment not applied for UPNP`); } } } catch (error) { @@ -73,15 +116,17 @@ async function adjustFirewallForUPNP() { /** * To verify that a port has UPnP (Universal Plug and Play) support. - * @param {number} apiport Port number. * @returns {Promise} True if port mappings can be set. Otherwise false. */ -async function verifyUPNPsupport(apiport = config.server.apiport) { +async function verifyUPNPsupport() { + const { routerIp } = userconfig.computed; + const { apiPort } = userconfig.computed; + const testPort = apiPort + 3; + try { - if (userconfig.initial.routerIP) { + if (routerIp) { await adjustFirewallForUPNP(); } - // run test on apiport + 1 await client.getPublicIp(); } catch (error) { log.error(error); @@ -99,8 +144,8 @@ async function verifyUPNPsupport(apiport = config.server.apiport) { } try { await client.createMapping({ - public: +apiport + 3, - private: +apiport + 3, + public: testPort, + private: testPort, ttl: 0, description: 'Flux_UPNP_Mapping_Test', }); @@ -120,7 +165,7 @@ async function verifyUPNPsupport(apiport = config.server.apiport) { } try { await client.removeMapping({ - public: +apiport + 3, + public: testPort, }); } catch (error) { log.error(error); @@ -135,32 +180,31 @@ async function verifyUPNPsupport(apiport = config.server.apiport) { /** * To set up UPnP (Universal Plug and Play) support. - * @param {number} apiport Port number. * @returns {Promise} True if port mappings can be set. Otherwise false. */ -async function setupUPNP(apiport = config.server.apiport) { +async function setupUPNP() { try { await client.createMapping({ - public: +apiport, - private: +apiport, + public: userconfig.computed.homePort, + private: userconfig.computed.homePort, + ttl: 0, + description: 'Flux_Home_UI', + }); + await client.createMapping({ + public: userconfig.computed.apiPort, + private: userconfig.computed.apiPort, ttl: 0, // Some routers force low ttl if 0, indefinite/default is used. Flux refreshes this every 6 blocks ~ 12 minutes description: 'Flux_Backend_API', }); await client.createMapping({ - public: +apiport + 1, - private: +apiport + 1, + public: userconfig.computed.apiPortSsl, + private: userconfig.computed.apiPortSsl, ttl: 0, // Some routers force low ttl if 0, indefinite/default is used. Flux refreshes this every 6 blocks ~ 12 minutes description: 'Flux_Backend_API_SSL', }); await client.createMapping({ - public: +apiport - 1, - private: +apiport - 1, - ttl: 0, - description: 'Flux_Home_UI', - }); - await client.createMapping({ - public: +apiport + 2, - private: +apiport + 2, + public: userconfig.computed.syncthingPort, + private: userconfig.computed.syncthingPort, ttl: 0, description: 'Flux_Syncthing', }); @@ -222,6 +266,29 @@ async function removeMapUpnpPort(port) { } } +/** + * Removes any mappings on a node at startup + * @param {string} ip + * This nodes ip. Trying to remove a mapping for a host ip that doens't belong to this host, will error. + */ +async function cleanOldMappings(ip) { + const mappings = await client.getMappings(); + + // await in loop so we can bail early if we get an error + // eslint-disable-next-line no-restricted-syntax + for (const mapping of mappings) { + if (mapping.private.host === ip) { + try { + // eslint-disable-next-line no-await-in-loop + await client.removeMapping(mapping.public.port, mapping.protocol); + } catch (error) { + log.error(error); + return; + } + } + } +} + /** * To map a specified port and show a message if successfully mapped. Only accessible by admins and Flux team members. * @param {object} req Request. @@ -393,6 +460,7 @@ module.exports = { isUPNP, verifyUPNPsupport, setupUPNP, + cleanOldMappings, mapUpnpPort, removeMapUpnpPort, mapPortApi, @@ -401,4 +469,6 @@ module.exports = { getIpApi, getGatewayApi, adjustFirewallForUPNP, + ufwAllowSsdpforInit, + ufwRemoveAllowSsdpforInit, }; diff --git a/ZelBack/src/services/utils/daemonrpcClient.js b/ZelBack/src/services/utils/daemonrpcClient.js index b20c4b3781..73e89506bd 100644 --- a/ZelBack/src/services/utils/daemonrpcClient.js +++ b/ZelBack/src/services/utils/daemonrpcClient.js @@ -1,4 +1,3 @@ -/* global userconfig */ const daemonrpc = require('daemonrpc'); const fullnode = require('fullnode'); const config = require('config'); diff --git a/ZelBack/src/services/verificationHelperUtils.js b/ZelBack/src/services/verificationHelperUtils.js index 733d994852..a0fb86e3c9 100644 --- a/ZelBack/src/services/verificationHelperUtils.js +++ b/ZelBack/src/services/verificationHelperUtils.js @@ -1,4 +1,3 @@ -/* global userconfig */ /** * @module * Contains utility functions to be used only by verificationHelper. diff --git a/apiServer.js b/apiServer.js index cbe9dc4937..55dd009198 100644 --- a/apiServer.js +++ b/apiServer.js @@ -1,4 +1,3 @@ -/* global userconfig */ global.userconfig = require('./config/userconfig'); process.env.NODE_CONFIG_DIR = `${__dirname}/ZelBack/config/`; @@ -14,33 +13,149 @@ const log = require('./ZelBack/src/lib/log'); const socket = require('./ZelBack/src/lib/socket'); const serviceManager = require('./ZelBack/src/services/serviceManager'); const upnpService = require('./ZelBack/src/services/upnpService'); +const fpcService = require('./ZelBack/src/services/fluxportControllerService'); + const hash = require('object-hash'); const { watch } = require('fs/promises'); const cmdAsync = util.promisify(nodecmd.get); -const apiPort = userconfig.initial.apiport || config.server.apiport; -const apiPortHttps = +apiPort + 1; + let initialHash = hash(fs.readFileSync(path.join(__dirname, '/config/userconfig.js'))); -async function loadUpnpIfRequired() { - let verifyUpnp = false; - let setupUpnp = false; - if (userconfig.initial.apiport) { - verifyUpnp = await upnpService.verifyUPNPsupport(apiPort); - if (verifyUpnp) { - setupUpnp = await upnpService.setupUPNP(apiPort); - } +function validIpv4Address(ip) { + const ipv4Regex = /^([1-9]\d{0,2}\.){3}[1-9]\d{0,2}$/; + + if (!ipv4Regex.test(ip)) return false; + + const octets = ip.split('.'); + const isValid = octets.every((octet) => parseInt(octet, 10) < 256); + return isValid; +} + +function validateTags() { + const tags = userconfig.initial.tags || {}; + + if (tags && tags.constructor !== Object) { + log.error('Error tags must be a mapping with string keys and values as string, number or boolean.'); + return {}; } - if ((userconfig.initial.apiport && userconfig.initial.apiport !== config.server.apiport) || userconfig.initial.routerIP) { - if (verifyUpnp !== true) { - log.error(`Flux port ${userconfig.initial.apiport} specified but UPnP failed to verify support. Shutting down.`); - process.exit(); + + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(tags)) { + const valuePassed = typeof value === 'string' || value instanceof String + || typeof value === 'number' || value instanceof Number + || typeof value === 'boolean' || value instanceof Boolean; + + if (!(typeof key === 'string' || key instanceof String) || !valuePassed) { + log.error('Tag must be a string and value must be a boolean, string or number, Skipping.'); + delete tags[key]; } - if (setupUpnp !== true) { - log.error(`Flux port ${userconfig.initial.apiport} specified but UPnP failed to map to api or home port. Shutting down.`); + } + + return tags; +} + +async function waitForApiPortAndRouterIp(autoUpnp) { + if (!autoUpnp) { + // if initial is undefined or empty string, use server.apiport + const apiPort = +userconfig.initial.apiport || +config.server.apiport; + const routerIp = userconfig.initial.routerIP; + if (routerIp && !validIpv4Address(routerIp)) { + log.error(`Router IP: ${routerIp} must be a valid ipv4 address.`); process.exit(); } + return [apiPort, routerIp]; + } + + if (await fpcService.startGossipServer()) { + const apiPort = fpcService.getApiPort(); + const routerIp = fpcService.getRouterIp(); + return Promise.all([apiPort, routerIp]); } + + log.error('Error starting GossipServer for autoUPnP. Unable to get collateral ' + + 'information, or unable to adjust firewall. Shutting down'); + return process.exit(); +} + +async function loadUpnpIfSupported(autoUpnp) { + let upnpSupported = false; + let upnpSetupComplete = false; + + const upnpRequested = Boolean( + autoUpnp + || userconfig.initial.routerIP + || (userconfig.initial.apiport && userconfig.initial.apiport !== config.server.apiport), + ); + + // Prior, this would run if `apiport` was set in config file. However, even if this + // wasn't set, after flux uppdated pgp or something, it would write the server default of 16127 to + // the config file, effectively making this always run as apiport was always true. So lets just + // run it by default + upnpSupported = await upnpService.verifyUPNPsupport(); + + if (upnpSupported) { + upnpSetupComplete = await upnpService.setupUPNP(); + } + + if (upnpSetupComplete || !upnpRequested) return; + + if (!upnpSupported) { + log.error(`Flux port ${userconfig.computed.apiPort} specified but UPnP failed to verify support. Shutting down.`); + } else if (!upnpSetupComplete) { + log.error(`Flux port ${userconfig.computed.apiPort} specified but UPnP failed to map to api or home port. Shutting down.`); + } + + process.exit(); +} + +async function SetupPortsUpnpAndComputed() { + if (!userconfig.computed) userconfig.computed = {}; + + const tags = validateTags(); + const autoUpnp = userconfig.initial.upnp || false; + + const homeDirPath = path.join(__dirname, '../'); + const newBenchmarkPath = path.join(homeDirPath, '.fluxbenchmark'); + const oldBenchmarkPath = path.join(homeDirPath, '.zelbenchmark'); + const isNewBenchPath = fs.existsSync(newBenchmarkPath); + const benchmarkPath = isNewBenchPath ? newBenchmarkPath : oldBenchmarkPath; + const benchmarkFile = isNewBenchPath ? 'fluxbench.conf' : 'zelbench.conf'; + const benchmarkConfigFilePath = path.join(benchmarkPath, benchmarkFile); + + userconfig.computed.benchmarkConfigFilePath = benchmarkConfigFilePath; + userconfig.computed.benchmarkPath = benchmarkPath; + userconfig.computed.isNewBenchPath = isNewBenchPath; + + userconfig.computed.homeDirPath = homeDirPath; + userconfig.computed.appRootPath = __dirname; + + userconfig.computed.tags = tags; + + let apiPort; + let routerIp; + + try { + [apiPort, routerIp] = await waitForApiPortAndRouterIp(autoUpnp); + } catch (err) { + log.error('Error waiting for ip and port, Shutting down.'); + log.error(err); + process.exit(); + } + + if (!config.server.allowedPorts.includes(apiPort)) { + log.error(`Flux port ${apiPort} is not supported. Shutting down.`); + process.exit(); + } + + userconfig.computed.homePort = apiPort - 1; + userconfig.computed.apiPort = apiPort; + userconfig.computed.apiPortSsl = apiPort + 1; + userconfig.computed.syncthingPort = apiPort + 2; + + userconfig.computed.routerIp = routerIp; + + await loadUpnpIfSupported(autoUpnp); } async function configReload() { @@ -56,9 +171,11 @@ async function configReload() { initialHash = hashCurrent; log.info(`Config file changed, reloading ${event.filename}...`); delete require.cache[require.resolve('./config/userconfig')]; + // only reimport initial - so we don't overwrite computed as other + // routines may be using this // eslint-disable-next-line - userconfig = require('./config/userconfig'); - await loadUpnpIfRequired(); + userconfig.initial = require('./config/userconfig').initial; + await SetupPortsUpnpAndComputed(); } } } catch (error) { @@ -68,20 +185,16 @@ async function configReload() { /** * - * @returns {Promise} + * @returns {Promise} */ async function initiate() { - if (!config.server.allowedPorts.includes(+apiPort)) { - log.error(`Flux port ${apiPort} is not supported. Shutting down.`); - process.exit(); - } - - await loadUpnpIfRequired(); + await SetupPortsUpnpAndComputed(); setInterval(async () => { configReload(); }, 2 * 1000); + const { apiPort } = userconfig.computed; const server = app.listen(apiPort, () => { log.info(`Flux listening on port ${apiPort}!`); serviceManager.startFluxFunctions(); @@ -100,15 +213,19 @@ async function initiate() { const cert = fs.readFileSync(path.join(__dirname, './certs/v1.crt'), 'utf8'); const credentials = { key, cert }; const httpsServer = https.createServer(credentials, app); - httpsServer.listen(apiPortHttps, () => { - log.info(`Flux https listening on port ${apiPortHttps}!`); + const { apiPortSsl } = userconfig.computed; + httpsServer.listen(apiPortSsl, () => { + log.info(`Flux https listening on port ${apiPortSsl}!`); }); } catch (error) { log.error(error); } - return apiPort; } module.exports = { initiate, + validIpv4Address, + validateTags, + waitForApiPortAndRouterIp, + loadUpnpIfSupported, }; diff --git a/app.js b/app.js index 24ebb3c4b6..a6083334c1 100644 --- a/app.js +++ b/app.js @@ -7,12 +7,14 @@ const express = require('express'); const apiServer = require('./apiServer'); async function initiate() { - const apiPort = await apiServer.initiate(); + await apiServer.initiate(); + if (process.argv[2] === '--dev') { log.info('Running FluxOS development server.'); return; } - const homePort = +apiPort - 1; + + const { homePort } = userconfig.computed; // Flux Home configuration const home = path.join(__dirname, './HomeUI/dist'); diff --git a/package.json b/package.json index 5df2fefb38..2a6df30c53 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,8 @@ "docs": "jsdoc -d docs --configure jsconf.json -r" }, "dependencies": { - "@runonflux/nat-upnp": "~1.0.2", + "@megachips/fluxport-controller": "^0.1.1", + "@megachips/nat-upnp": "^1.1.1", "apicache": "~1.6.3", "archiver": "~6.0.1", "axios": "~0.27.2", diff --git a/tests/unit/apiServer.test.js b/tests/unit/apiServer.test.js new file mode 100644 index 0000000000..11f3159859 --- /dev/null +++ b/tests/unit/apiServer.test.js @@ -0,0 +1,202 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyRequire = require('proxyquire').noPreserveCache(); + +const config = { + initial: { + apiport: '16137', + routerIP: '1.2.3.4', + }, + computed: { + appRootPath: 'testpath', // for fluxbench file in fluxportController + }, +}; + +let apiServer = proxyRequire('../../apiServer', { './config/userconfig': structuredClone(config) }); + +// require these after apiServer as daemonService needs config +const fluxportControllerService = require('../../ZelBack/src/services/fluxportControllerService'); +const upnpService = require('../../ZelBack/src/services/upnpService'); + +// computed: { +// benchmarkConfigFilePath: 'benchpath', +// appRootPath: '/zelflux', +// homePort: 16136, +// apiPort: 16137, +// apiPortSsl: 16138, +// syncthingPort: 16139, +// }, + +const log = require('../../ZelBack/src/lib/log'); + +describe('apiServer tests', () => { + let errorSpy; + + beforeEach(() => { + errorSpy = sinon.spy(log, 'error'); + apiServer = proxyRequire('../../apiServer', { './config/userconfig': structuredClone(config) }); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Valid tags / ip addresses tests', () => { + it('should validate IP addresses correctly', async () => { + const addrs = ['1.2.3.4', '111.222.222.111', 1234, '1.2d.3.4', '122.223.231.288']; + const results = addrs.map((addr) => apiServer.validIpv4Address(addr)); + expect(results).to.deep.equal([true, true, false, false, false]); + }); + + it('should validate tags correctly', async () => { + const valid = { + ValidTag1: 'testnode', + ValidTag2: 123445, + ValidTag3: true, + }; + const invalid = { + InvalidTag1: [], + InvalidTag2: {}, + }; + + userconfig.initial.tags = { ...valid, ...invalid }; + + const tags = apiServer.validateTags(); + expect(tags).to.be.deep.equal(valid); + sinon.assert.callCount(errorSpy, 2); + sinon.assert.calledWithExactly(errorSpy, 'Tag must be a string and value must be a boolean, string or number, Skipping.'); + }); + + it('should log error and return empty tags object for incorrect type', async () => { + const invalidArgs = [[], 'Name=Value', 12345, new Set(), new Map()]; + + invalidArgs.forEach((invalidArg) => { + userconfig.initial.tags = invalidArg; + const tags = apiServer.validateTags(); + expect(tags).to.deep.equal({}); + }); + + sinon.assert.callCount(errorSpy, 5); + sinon.assert.calledWithExactly(errorSpy, 'Error tags must be a mapping with string keys and values as string, number or boolean.'); + }); + }); + + describe('wait for apiport / routerip tests', () => { + it('Should return port / ip with correct type from userconfig if autoUpnp not set', async () => { + const result = await apiServer.waitForApiPortAndRouterIp(false); + expect(result).to.deep.equal([16137, '1.2.3.4']); + }); + + it('Should log error and exit if invalid ip address used in config', async () => { + const exit = sinon.stub(process, 'exit'); + + userconfig.initial.routerIP = '444.444.444.444'; + await apiServer.waitForApiPortAndRouterIp(false); + sinon.assert.calledOnceWithExactly(errorSpy, 'Router IP: 444.444.444.444 must be a valid ipv4 address.'); + sinon.assert.calledOnce(exit); + }); + + it("Should start gossip server and return it's ip / port instead of userconfig if upnp set in config", async () => { + const startServer = sinon.stub(fluxportControllerService, 'startGossipServer'); + const getPort = sinon.stub(fluxportControllerService, 'getApiPort'); + const getIp = sinon.stub(fluxportControllerService, 'getRouterIp'); + + startServer.resolves(true); + getPort.returns(16157); + getIp.returns('3.3.3.3'); + + const result = await apiServer.waitForApiPortAndRouterIp(true); + expect(result).to.deep.equal([16157, '3.3.3.3']); + sinon.assert.calledOnce(startServer); + sinon.assert.calledOnce(getPort); + sinon.assert.calledOnce(getIp); + sinon.assert.notCalled(errorSpy); + }); + + it("Should log error and exit if gossipServer doesn't start", async () => { + const exit = sinon.stub(process, 'exit'); + const startServer = sinon.stub(fluxportControllerService, 'startGossipServer'); + startServer.resolves(false); + + await apiServer.waitForApiPortAndRouterIp(true); + sinon.assert.calledOnce(exit); + sinon.assert.calledOnceWithExactly(errorSpy, 'Error starting GossipServer for autoUPnP. Unable to get collateral ' + + 'information, or unable to adjust firewall. Shutting down'); + }); + }); + describe('LoadUpnpIfSupported tests', () => { + it('Should verify and setup upnp port if routerIP set in userconfig', async () => { + const verify = sinon.stub(upnpService, 'verifyUPNPsupport'); + const setup = sinon.stub(upnpService, 'setupUPNP'); + + verify.resolves(true); + setup.resolves(true); + + await apiServer.loadUpnpIfSupported(false); + sinon.assert.calledOnce(verify); + sinon.assert.calledOnce(setup); + sinon.assert.notCalled(errorSpy); + }); + it('Should verify and setup upnp port if only upnp is set in userconfig', async () => { + const verify = sinon.stub(upnpService, 'verifyUPNPsupport'); + const setup = sinon.stub(upnpService, 'setupUPNP'); + + verify.resolves(true); + setup.resolves(true); + + delete userconfig.initial.routerIP; + delete userconfig.initial.apiport; + userconfig.initial.upnp = true; + + await apiServer.loadUpnpIfSupported(true); + sinon.assert.calledOnce(verify); + sinon.assert.calledOnce(setup); + sinon.assert.notCalled(errorSpy); + }); + it('Should verify and return silently if apiport set to 16127 and upnp not supported', async () => { + const verify = sinon.stub(upnpService, 'verifyUPNPsupport'); + const setup = sinon.stub(upnpService, 'setupUPNP'); + + verify.resolves(false); + + delete userconfig.initial.routerIP; + userconfig.initial.apiport = 16127; + + await apiServer.loadUpnpIfSupported(false); + sinon.assert.calledOnce(verify); + sinon.assert.notCalled(setup); + sinon.assert.notCalled(errorSpy); + }); + it('Should log error and exit if upnp requested and not supported', async () => { + const exit = sinon.stub(process, 'exit'); + const verify = sinon.stub(upnpService, 'verifyUPNPsupport'); + + verify.resolves(false); + + userconfig.initial.routerIP = undefined; + userconfig.computed.apiPort = 16137; + + await apiServer.loadUpnpIfSupported(false); + sinon.assert.calledOnce(verify); + sinon.assert.calledOnce(exit); + sinon.assert.calledOnceWithExactly(errorSpy, 'Flux port 16137 specified but UPnP failed to verify support. Shutting down.'); + }); + it('Should log error and exit if upnp requested and failed setup', async () => { + const exit = sinon.stub(process, 'exit'); + const verify = sinon.stub(upnpService, 'verifyUPNPsupport'); + const setup = sinon.stub(upnpService, 'setupUPNP'); + + verify.resolves(true); + setup.resolves(false); + + userconfig.initial.routerIP = undefined; + userconfig.computed.apiPort = 16137; + + await apiServer.loadUpnpIfSupported(false); + sinon.assert.calledOnce(verify); + sinon.assert.calledOnce(setup); + sinon.assert.calledOnce(exit); + sinon.assert.calledOnceWithExactly(errorSpy, 'Flux port 16137 specified but UPnP failed to map to api or home port. Shutting down.'); + }); + }); +}); diff --git a/tests/unit/fluxportControllerService.test.js b/tests/unit/fluxportControllerService.test.js new file mode 100644 index 0000000000..5365ea3bf8 --- /dev/null +++ b/tests/unit/fluxportControllerService.test.js @@ -0,0 +1,150 @@ +global.userconfig = require('../../config/userconfig'); +const chai = require('chai'); +const sinon = require('sinon'); +const log = require('../../ZelBack/src/lib/log'); +const generalService = require('../../ZelBack/src/services/generalService'); +const upnpService = require('../../ZelBack/src/services/upnpService'); +const fluxportControllerService = require('../../ZelBack/src/services/fluxportControllerService'); +const fpc = require('@megachips/fluxport-controller'); +const fs = require('node:fs/promises'); +const benchService = require('../../ZelBack/src/services/benchmarkService'); + +const { expect } = chai; + +describe('fluxportControllerService tests', () => { + describe('startGossipServer tests', () => { + let errorSpy; + + beforeEach(() => { + errorSpy = sinon.spy(log, 'error'); + + global.userconfig = { + computed: { + benchmarkConfigFilePath: 'benchpath', + appRootPath: '/zelflux', + homePort: 16136, + apiPort: 16137, + apiPortSsl: 16138, + syncthingPort: 16139, + }, + }; + }); + + afterEach(() => { + sinon.stub(fpc.FluxGossipServer.prototype, 'stop'); + fluxportControllerService.stopGossipServer(); + sinon.restore(); + }); + + it('should start GossipServer and not raise errors', async () => { + sinon.stub(generalService, 'obtainNodeCollateralInformation').returns(Promise.resolve({ txhash: 'testtx', txindex: 0 })); + sinon.stub(upnpService, 'ufwAllowSsdpforInit').returns(Promise.resolve(true)); + const gossipStart = sinon.stub(fpc.FluxGossipServer.prototype, 'start'); + + const pre = fluxportControllerService.getGossipServer(); + const gossip = await fluxportControllerService.startGossipServer(); + + expect(pre).to.equal(null); + expect(gossip).to.be.ok; + expect(gossip.startedAt).to.equal(0); + sinon.assert.notCalled(errorSpy); + sinon.assert.calledOnce(generalService.obtainNodeCollateralInformation); + sinon.assert.calledOnce(gossipStart); + }); + it('should only start once', async () => { + const infoStub = sinon.stub(log, 'info'); + + sinon.stub(generalService, 'obtainNodeCollateralInformation').returns(Promise.resolve({ txhash: 'testtx', txindex: 0 })); + sinon.stub(upnpService, 'ufwAllowSsdpforInit').returns(Promise.resolve(true)); + sinon.stub(fpc.FluxGossipServer.prototype, 'start'); + + await fluxportControllerService.startGossipServer(); + const first = fluxportControllerService.getGossipServer(); + await fluxportControllerService.startGossipServer(); + const second = fluxportControllerService.getGossipServer(); + const startLogs = infoStub.getCalls().filter( + (call) => call.calledWithExactly('Starting GossipServer'), + ); + + expect(first).to.equal(second); + expect(startLogs.length).to.equal(1); + sinon.assert.notCalled(errorSpy); + }); + it('should attach routerIp and apiPort listeners', async () => { + sinon.stub(generalService, 'obtainNodeCollateralInformation').returns(Promise.resolve({ txhash: 'testtx', txindex: 0 })); + sinon.stub(upnpService, 'ufwAllowSsdpforInit').returns(Promise.resolve(true)); + sinon.stub(fpc.FluxGossipServer.prototype, 'start'); + + const gossip = await fluxportControllerService.startGossipServer(); + + const routerListeners = gossip.listenerCount('routerIpConfirmed'); + const ipListeners = gossip.listenerCount('portConfirmed'); + + expect(routerListeners).to.equal(1); + expect(ipListeners).to.equal(1); + sinon.assert.notCalled(errorSpy); + }); + it('should handle a routerIpConfirmed event', async () => { + const infoStub = sinon.stub(log, 'info'); + + sinon.stub(generalService, 'obtainNodeCollateralInformation').returns(Promise.resolve({ txhash: 'testtx', txindex: 0 })); + sinon.stub(upnpService, 'ufwAllowSsdpforInit').returns(Promise.resolve(true)); + + const remove = sinon.stub(upnpService, 'ufwRemoveAllowSsdpforInit'); + remove.returns(Promise.resolve()); + + const clean = sinon.stub(upnpService, 'cleanOldMappings'); + clean.returns(Promise.resolve()); + + sinon.stub(fpc.FluxGossipServer.prototype, 'start'); + + const gossip = await fluxportControllerService.startGossipServer(); + + gossip.emit('routerIpConfirmed', '10.10.123.123'); + + gossip.on('flush', async () => { + const ipLogs = infoStub.getCalls().filter( + (call) => call.calledWithExactly('Gossip server got new routerIp: 10.10.123.123, updating'), + ); + + expect(ipLogs.length).to.equal(1); + sinon.assert.calledOnce(remove); + sinon.assert.calledOnceWithExactly(clean, '10.10.123.123'); + sinon.assert.notCalled(errorSpy); + expect(await fluxportControllerService.getRouterIp()).to.equal('10.10.123.123'); + }); + }); + it('should handle a portConfirmed event', async () => { + const infoStub = sinon.stub(log, 'info'); + + sinon.stub(generalService, 'obtainNodeCollateralInformation').returns(Promise.resolve({ txhash: 'testtx', txindex: 0 })); + sinon.stub(upnpService, 'ufwAllowSsdpforInit').returns(Promise.resolve(true)); + + const readFile = sinon.stub(fs, 'readFile'); + readFile.returns(Promise.resolve('fluxport=16137')); + const writeFile = sinon.stub(fs, 'writeFile'); + writeFile.resolves(null); + const executeBench = sinon.stub(benchService, 'executeCall'); + executeBench.resolves(null); + + sinon.stub(fpc.FluxGossipServer.prototype, 'start'); + + const gossip = await fluxportControllerService.startGossipServer(); + + gossip.emit('portConfirmed', '1234'); + + gossip.on('flush', async () => { + const ipLogs = infoStub.getCalls().filter( + (call) => call.calledWithExactly('Gossip server got new apiPort: 1234, updating'), + ); + + expect(ipLogs.length).to.equal(1); + sinon.assert.calledOnce(readFile); + sinon.assert.calledOnceWithExactly(writeFile, 'fluxport=1234'); + sinon.assert.calledOnceWithExactly(executeBench, 'restartnodebenchmarks'); + sinon.assert.notCalled(errorSpy); + expect(await fluxportControllerService.getApiPort()).to.equal('1234'); + }); + }); + }); +}); diff --git a/tests/unit/upnpService.test.js b/tests/unit/upnpService.test.js index cece94dd45..31652b11b2 100644 --- a/tests/unit/upnpService.test.js +++ b/tests/unit/upnpService.test.js @@ -1,18 +1,14 @@ /* eslint-disable class-methods-use-this */ /* eslint-disable no-restricted-syntax */ const chai = require('chai'); -const natUpnp = require('@runonflux/nat-upnp'); +const natUpnp = require('@megachips/nat-upnp'); const sinon = require('sinon'); -const proxyquire = require('proxyquire'); const log = require('../../ZelBack/src/lib/log'); const verificationHelper = require('../../ZelBack/src/services/verificationHelper'); +const upnpService = require('../../ZelBack/src/services/upnpService'); const { expect } = chai; -const config = { - apiport: '5550', -}; - const generateResponse = () => { const res = { test: 'testing' }; res.status = sinon.stub().returns(res); @@ -24,17 +20,21 @@ const generateResponse = () => { return res; }; -const upnpService = proxyquire( - '../../ZelBack/src/services/upnpService', - { config }, -); - describe('upnpService tests', () => { describe('verifyUPNPsupport tests', () => { let logSpy; beforeEach(() => { logSpy = sinon.spy(log, 'error'); + + global.userconfig = { + computed: { + homePort: 16136, + apiPort: 16137, + apiPortSsl: 16138, + syncthingPort: 16139, + }, + }; }); afterEach(() => { @@ -132,6 +132,15 @@ describe('upnpService tests', () => { beforeEach(() => { logSpy = sinon.spy(log, 'error'); createMappingSpy = sinon.stub(natUpnp.Client.prototype, 'createMapping'); + + global.userconfig = { + computed: { + homePort: 122, + apiPort: 123, + apiPortSsl: 124, + syncthingPort: 125, + }, + }; }); afterEach(() => { @@ -141,44 +150,22 @@ describe('upnpService tests', () => { it('should return true if all client responses are valid', async () => { createMappingSpy.returns(true); - const result = await upnpService.setupUPNP(123); - - expect(result).to.equal(true); - sinon.assert.notCalled(logSpy); - sinon.assert.callCount(createMappingSpy, 4); - sinon.assert.calledWithExactly(createMappingSpy, { - public: 123, private: 123, ttl: 0, description: 'Flux_Backend_API', - }); - sinon.assert.calledWithExactly(createMappingSpy, { - public: 124, private: 124, ttl: 0, description: 'Flux_Backend_API_SSL', - }); - sinon.assert.calledWithExactly(createMappingSpy, { - public: 122, private: 122, ttl: 0, description: 'Flux_Home_UI', - }); - sinon.assert.calledWithExactly(createMappingSpy, { - public: 125, private: 125, ttl: 0, description: 'Flux_Syncthing', - }); - }); - - it('should return true if all client responses are valid, no parameter passed', async () => { - createMappingSpy.returns(true); - const result = await upnpService.setupUPNP(); expect(result).to.equal(true); sinon.assert.notCalled(logSpy); sinon.assert.callCount(createMappingSpy, 4); sinon.assert.calledWithExactly(createMappingSpy, { - public: 16127, private: 16127, ttl: 0, description: 'Flux_Backend_API', + public: 122, private: 122, ttl: 0, description: 'Flux_Home_UI', }); sinon.assert.calledWithExactly(createMappingSpy, { - public: 16128, private: 16128, ttl: 0, description: 'Flux_Backend_API_SSL', + public: 123, private: 123, ttl: 0, description: 'Flux_Backend_API', }); sinon.assert.calledWithExactly(createMappingSpy, { - public: 16126, private: 16126, ttl: 0, description: 'Flux_Home_UI', + public: 124, private: 124, ttl: 0, description: 'Flux_Backend_API_SSL', }); sinon.assert.calledWithExactly(createMappingSpy, { - public: 16129, private: 16129, ttl: 0, description: 'Flux_Syncthing', + public: 125, private: 125, ttl: 0, description: 'Flux_Syncthing', }); });