diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index ff32111f5..34c47b2cb 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -50,12 +50,14 @@ module.exports = { // Nodes adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler); - if (!settings.editorTheme || !settings.editorTheme.palette || settings.editorTheme.palette.upload !== false) { - const multer = require('multer'); - const upload = multer({ storage: multer.memoryStorage() }); - adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler); - } else { - adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler); + if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) { + if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowUpload !== false) { + const multer = require('multer'); + const upload = multer({ storage: multer.memoryStorage() }); + adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler); + } else { + adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler); + } } adminApp.get(/^\/nodes\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalogs,apiUtil.errorHandler); adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+\/[^\/]+)\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalog,apiUtil.errorHandler); diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index ff8dad5bf..97159e6d2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -269,7 +269,7 @@ var RED = (function() { } } ] - // } else if (RED.settings.theme('palette.editable') !== false) { + // } else if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) { } else { options.buttons = [ { @@ -509,7 +509,7 @@ var RED = (function() { ]}); menuOptions.push(null); - if (RED.settings.theme('palette.editable') !== false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) { menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"}); menuOptions.push(null); } @@ -544,7 +544,7 @@ var RED = (function() { RED.palette.init(); RED.eventLog.init(); - if (RED.settings.theme('palette.editable') !== false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) { RED.palette.editor.init(); } else { console.log("Palette editor disabled"); diff --git a/packages/node_modules/@node-red/editor-client/src/js/settings.js b/packages/node_modules/@node-red/editor-client/src/js/settings.js index 760165581..109554418 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/settings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/settings.js @@ -57,12 +57,11 @@ RED.settings = (function () { return JSON.parse(localStorage.getItem(key)); } else { var v; - try { - v = RED.utils.getMessageProperty(userSettings,key); - if (v === undefined) { - v = defaultIfUndefined; - } - } catch(err) { + try { v = RED.utils.getMessageProperty(userSettings,key); } catch(err) {} + if (v === undefined) { + try { v = RED.utils.getMessageProperty(RED.settings,key); } catch(err) {} + } + if (v === undefined) { v = defaultIfUndefined; } return v; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index b0aeed0fe..3ff3e7436 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -329,21 +329,26 @@ RED.palette.editor = (function() { catalogueLoadStatus.push(err||v); if (!err) { if (v.modules) { - v.modules.forEach(function(m) { - loadedIndex[m.id] = m; - m.index = [m.id]; - if (m.keywords) { - m.index = m.index.concat(m.keywords); + var a = false; + v.modules = v.modules.filter(function(m) { + if (checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) { + loadedIndex[m.id] = m; + m.index = [m.id]; + if (m.keywords) { + m.index = m.index.concat(m.keywords); + } + if (m.types) { + m.index = m.index.concat(m.types); + } + if (m.updated_at) { + m.timestamp = new Date(m.updated_at).getTime(); + } else { + m.timestamp = 0; + } + m.index = m.index.join(",").toLowerCase(); + return true; } - if (m.types) { - m.index = m.index.concat(m.types); - } - if (m.updated_at) { - m.timestamp = new Date(m.updated_at).getTime(); - } else { - m.timestamp = 0; - } - m.index = m.index.join(",").toLowerCase(); + return false; }) loadedList = loadedList.concat(v.modules); } @@ -437,11 +442,84 @@ RED.palette.editor = (function() { return -1 * (A.info.timestamp-B.info.timestamp); } + var installAllowList = ['*']; + var installDenyList = []; + + function parseModuleList(list) { + list = list || ["*"]; + return list.map(function(rule) { + var m = /^(.+?)(?:@(.*))?$/.exec(rule); + var wildcardPos = m[1].indexOf("*"); + wildcardPos = wildcardPos===-1?Infinity:wildcardPos; + + return { + module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), + version: m[2], + wildcardPos: wildcardPos + } + }) + } + + function checkAgainstList(module,version,list) { + for (var i=0;i deniedRule.wildcardPos + } else { + // First wildcard in same position. + // Go with the longer matching rule. This isn't going to be 100% + // right, but we are deep into edge cases at this point. + return allowedRule.module.toString().length > deniedRule.module.toString().length + } + return false; + } function init() { - if (RED.settings.theme('palette.editable') === false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { return; } + var settingsAllowList = RED.settings.get("externalModules.palette.allowList") + var settingsDenyList = RED.settings.get("externalModules.palette.denyList") + if (settingsAllowList || settingsDenyList) { + installAllowList = settingsAllowList; + installDenyList = settingsDenyList + } + installAllowList = parseModuleList(installAllowList); + installDenyList = parseModuleList(installDenyList); + createSettingsPane(); RED.userSettings.add({ @@ -880,7 +958,7 @@ RED.palette.editor = (function() { } }); - if (RED.settings.theme('palette.upload') !== false) { + if (RED.settings.get('externalModules.palette.allowUpload', true) !== false) { var uploadSpan = $('').prependTo(toolBar); var uploadButton = $('').appendTo(uploadSpan); @@ -962,7 +1040,7 @@ RED.palette.editor = (function() { } function update(entry,version,url,container,done) { - if (RED.settings.theme('palette.editable') === false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { done(new Error('Palette not editable')); return; } @@ -1021,7 +1099,7 @@ RED.palette.editor = (function() { }) } function remove(entry,container,done) { - if (RED.settings.theme('palette.editable') === false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { done(new Error('Palette not editable')); return; } @@ -1078,7 +1156,7 @@ RED.palette.editor = (function() { }) } function install(entry,container,done) { - if (RED.settings.theme('palette.editable') === false) { + if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { done(new Error('Palette not editable')); return; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js index fdd106e23..c39ac97be 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js @@ -465,7 +465,7 @@ RED.projects.settings = (function() { metaRow = $('
').appendTo(headerRow); var buttons = $('
').appendTo(metaRow); if (RED.user.hasPermission("projects.write")) { - if (!entry.installed && RED.settings.theme('palette.editable') !== false) { + if (!entry.installed && RED.settings.get('externalModules.palette.allowInstall', true) !== false) { $('' + RED._("sidebar.project.projectSettings.install") + '').appendTo(buttons) .on("click", function(evt) { evt.preventDefault(); diff --git a/packages/node_modules/@node-red/registry/lib/index.js b/packages/node_modules/@node-red/registry/lib/index.js index 3632f40cf..426dfcb61 100644 --- a/packages/node_modules/@node-red/registry/lib/index.js +++ b/packages/node_modules/@node-red/registry/lib/index.js @@ -254,7 +254,8 @@ module.exports = { * Update to internal list of available modules based on what has been actually * loaded. * - * The `autoInstallModules` runtime option means the runtime may try to install + * The `externalModules.autoInstall` (previously `autoInstallModules`) + * runtime option means the runtime may try to install * missing modules after the initial load is complete. If that flag is not set * this function is used to remove the modules from the registry's saved list. * @function diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 0c3359382..c7cd0d5e0 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -15,26 +15,55 @@ **/ -var path = require("path"); -var os = require("os"); -var fs = require("fs-extra"); -var tar = require("tar"); +const path = require("path"); +const os = require("os"); +const fs = require("fs-extra"); +const tar = require("tar"); -var registry = require("./registry"); -var library = require("./library"); +const registry = require("./registry"); +const registryUtil = require("./util"); +const library = require("./library"); const {exec,log,events} = require("@node-red/util"); -var child_process = require('child_process'); -var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -var installerEnabled = false; +const child_process = require('child_process'); +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +let installerEnabled = false; -var settings; +let settings; const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/; +// Default allow/deny lists +let installAllowList = ['*']; +let installDenyList = []; +let installAllAllowed = true; +let installVersionRestricted = false; + function init(_settings) { settings = _settings; + // TODO: This is duplicated in localfilesystem.js + // Should it *all* be managed by util? + if (settings.externalModules && settings.externalModules.palette) { + if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { + installAllowList = settings.externalModules.palette.allowList; + installDenyList = settings.externalModules.palette.denyList; + } + } + installAllowList = registryUtil.parseModuleList(installAllowList); + installDenyList = registryUtil.parseModuleList(installDenyList); + installAllAllowed = installDenyList.length === 0; + if (!installAllAllowed) { + installAllowList.forEach(function(rule) { + installVersionRestricted = installVersionRestricted || (!!rule.version); + }) + if (!installVersionRestricted) { + installDenyList.forEach(function(rule) { + installVersionRestricted = installVersionRestricted || (!!rule.version); + }) + + } + } } var activePromise = Promise.resolve(); @@ -79,93 +108,101 @@ function checkExistingModule(module,version) { return false; } -function installModule(module,version,url) { +async function installModule(module,version,url) { if (Buffer.isBuffer(module)) { return installTarball(module) } module = module || ""; - activePromise = activePromise.then(() => { + activePromise = activePromise.then(async function() { //TODO: ensure module is 'safe' - return new Promise((resolve,reject) => { - var installName = module; - var isUpgrade = false; - try { - if (url) { - if (pkgurlRe.test(url) || localtgzRe.test(url)) { - // Git remote url or Tarball url - check the valid package url - installName = url; - } else { - log.warn(log._("server.install.install-failed-url",{name:module,url:url})); - const e = new Error("Invalid url"); - e.code = "invalid_module_url"; - reject(e); - return; - } - } else if (moduleRe.test(module)) { - // Simple module name - assume it can be npm installed - if (version) { - installName += "@"+version; - } - } else if (slashRe.test(module)) { - // A path - check if there's a valid package.json - installName = module; - let info = checkModulePath(module); - module = info.name; - } else { - log.warn(log._("server.install.install-failed-name",{name:module})); - const e = new Error("Invalid module name"); - e.code = "invalid_module_name"; - reject(e); - return; - } - isUpgrade = checkExistingModule(module,version); - } catch(err) { - return reject(err); - } - if (!isUpgrade) { - log.info(log._("server.install.installing",{name: module,version: version||"latest"})); + var installName = module; + let isRegistryPackage = true; + var isUpgrade = false; + if (url) { + if (pkgurlRe.test(url) || localtgzRe.test(url)) { + // Git remote url or Tarball url - check the valid package url + installName = url; + isRegistryPackage = false; } else { - log.info(log._("server.install.upgrading",{name: module,version: version||"latest"})); + log.warn(log._("server.install.install-failed-url",{name:module,url:url})); + const e = new Error("Invalid url"); + e.code = "invalid_module_url"; + throw e; + } + } else if (moduleRe.test(module)) { + // Simple module name - assume it can be npm installed + if (version) { + installName += "@"+version; + } + } else if (slashRe.test(module)) { + // A path - check if there's a valid package.json + installName = module; + let info = checkModulePath(module); + module = info.name; + isRegistryPackage = false; + } else { + log.warn(log._("server.install.install-failed-name",{name:module})); + const e = new Error("Invalid module name"); + e.code = "invalid_module_name"; + throw e; + } + if (!installAllAllowed) { + let installVersion = version; + if (installVersionRestricted && isRegistryPackage) { + installVersion = await getModuleVersionFromNPM(module, version); } - var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; - var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName]; - log.trace(npmCommand + JSON.stringify(args)); - exec.run(npmCommand,args,{ - cwd: installDir - }, true).then(result => { - if (!isUpgrade) { - log.info(log._("server.install.installed",{name:module})); - resolve(require("./index").addModule(module).then(reportAddedModules)); - } else { - log.info(log._("server.install.upgraded",{name:module, version:version})); - events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); - resolve(require("./registry").setModulePendingUpdated(module,version)); - } - }).catch(result => { - var output = result.stderr; - var e; - var lookFor404 = new RegExp(" 404 .*"+module,"m"); - var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); - if (lookFor404.test(output)) { - log.warn(log._("server.install.install-failed-not-found",{name:module})); - e = new Error("Module not found"); - e.code = 404; - reject(e); - } else if (isUpgrade && lookForVersionNotFound.test(output)) { - log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); - e = new Error("Module not found"); - e.code = 404; - reject(e); - } else { - log.warn(log._("server.install.install-failed-long",{name:module})); - log.warn("------------------------------------------"); - log.warn(output); - log.warn("------------------------------------------"); - reject(new Error(log._("server.install.install-failed"))); - } - }) - }); + if (!registryUtil.checkModuleAllowed(module,installVersion,installAllowList,installDenyList)) { + const e = new Error("Install not allowed"); + e.code = "install_not_allowed"; + throw e; + } + } + isUpgrade = checkExistingModule(module,version); + + if (!isUpgrade) { + log.info(log._("server.install.installing",{name: module,version: version||"latest"})); + } else { + log.info(log._("server.install.upgrading",{name: module,version: version||"latest"})); + } + + var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; + var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName]; + log.trace(npmCommand + JSON.stringify(args)); + return exec.run(npmCommand,args,{ + cwd: installDir + }, true).then(result => { + if (!isUpgrade) { + log.info(log._("server.install.installed",{name:module})); + return require("./index").addModule(module).then(reportAddedModules); + } else { + log.info(log._("server.install.upgraded",{name:module, version:version})); + events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); + return require("./registry").setModulePendingUpdated(module,version); + } + }).catch(result => { + var output = result.stderr; + var e; + var lookFor404 = new RegExp(" 404 .*"+module,"m"); + var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); + if (lookFor404.test(output)) { + log.warn(log._("server.install.install-failed-not-found",{name:module})); + e = new Error("Module not found"); + e.code = 404; + throw e; + } else if (isUpgrade && lookForVersionNotFound.test(output)) { + log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); + e = new Error("Module not found"); + e.code = 404; + throw e; + } else { + log.warn(log._("server.install.install-failed-long",{name:module})); + log.warn("------------------------------------------"); + log.warn(output); + log.warn("------------------------------------------"); + throw new Error(log._("server.install.install-failed")); + } + }) }).catch(err => { // In case of error, reset activePromise to be resolvable activePromise = Promise.resolve(); @@ -214,7 +251,63 @@ async function getExistingPackageVersion(moduleName) { return null; } +async function getModuleVersionFromNPM(module, version) { + let installName = module; + if (version) { + installName += "@" + version; + } + + return new Promise((resolve, reject) => { + child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) { + try { + if (!stdout) { + log.warn(log._("server.install.install-failed-not-found",{name:module})); + e = new Error("Version not found"); + e.code = 404; + reject(e); + return; + } + const response = JSON.parse(stdout); + if (response.error) { + if (response.error.code === "E404") { + log.warn(log._("server.install.install-failed-not-found",{name:module})); + e = new Error("Module not found"); + e.code = 404; + reject(e); + } else { + log.warn(log._("server.install.install-failed-long",{name:module})); + log.warn("------------------------------------------"); + log.warn(response.error.summary); + log.warn("------------------------------------------"); + reject(new Error(log._("server.install.install-failed"))); + } + return; + } else { + resolve(response.version); + } + } catch(err) { + log.warn(log._("server.install.install-failed-long",{name:module})); + log.warn("------------------------------------------"); + if (stdout) { + log.warn(stdout); + } + if (stderr) { + log.warn(stderr); + } + log.warn(err); + log.warn("------------------------------------------"); + reject(new Error(log._("server.install.install-failed"))); + } + }); + }) +} + + async function installTarball(tarball) { + if (settings.externalModules && settings.externalModules.palette && settings.externalModules.palette.allowUpload === false) { + throw new Error("Module upload disabled") + } + // Check this tarball contains a valid node-red module. // Get its module name/version const moduleInfo = await getTarballModuleInfo(tarball); @@ -335,7 +428,32 @@ function uninstallModule(module) { return activePromise; } -function checkPrereq() { +async function checkPrereq() { + if (settings.editorTheme && settings.editorTheme.palette) { + if (settings.editorTheme.palette.hasOwnProperty("editable")) { + log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.editable", new:"externalModules.palette.allowInstall"})); + } + if (settings.editorTheme.palette.hasOwnProperty("upload")) { + log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.upload", new:"externalModules.palette.allowUpload"})); + } + } + + try { + if (settings.editorTheme.palette.editable === false) { + log.info(log._("server.palette-editor.disabled")); + installerEnabled = false; + return + } + } catch(err) {} + + try { + if (settings.externalModules.palette.allowInstall === false) { + log.info(log._("server.palette-editor.disabled")); + installerEnabled = false; + return + } + } catch(err) {} + if (settings.hasOwnProperty('editorTheme') && settings.editorTheme.hasOwnProperty('palette') && settings.editorTheme.palette.hasOwnProperty('editable') && @@ -343,7 +461,6 @@ function checkPrereq() { ) { log.info(log._("server.palette-editor.disabled")); installerEnabled = false; - return Promise.resolve(); } else { return new Promise(resolve => { child_process.execFile(npmCommand,['-v'],function(err,stdout) { diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index 7aea3f57b..25e423571 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -14,10 +14,16 @@ * limitations under the License. **/ -var fs = require("fs"); -var path = require("path"); -var log = require("@node-red/util").log; -var i18n = require("@node-red/util").i18n; +const fs = require("fs"); +const path = require("path"); +const log = require("@node-red/util").log; +const i18n = require("@node-red/util").i18n; +const registryUtil = require("./util"); + +// Default allow/deny lists +let loadAllowList = ['*']; +let loadDenyList = []; + var settings; var disableNodePathScan = false; @@ -25,6 +31,16 @@ var iconFileExtensions = [".png", ".gif", ".svg"]; function init(_settings) { settings = _settings; + // TODO: This is duplicated in installer.js + // Should it *all* be managed by util? + if (settings.externalModules && settings.externalModules.palette) { + if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { + loadAllowList = settings.externalModules.palette.allowList; + loadDenyList = settings.externalModules.palette.denyList; + } + } + loadAllowList = registryUtil.parseModuleList(loadAllowList); + loadDenyList = registryUtil.parseModuleList(loadDenyList); } function isIncluded(name) { @@ -137,8 +153,12 @@ function scanDirForNodesModules(dir,moduleName) { try { var pkg = require(pkgfn); if (pkg['node-red']) { - var moduleDir = path.join(dir,fn); - results.push({dir:moduleDir,package:pkg}); + if (!registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)) { + log.debug("! Module: "+pkg.name+" "+pkg.version+ " *ignored due to denyList*"); + } else { + var moduleDir = path.join(dir,fn); + results.push({dir:moduleDir,package:pkg}); + } } } catch(err) { if (err.code != "MODULE_NOT_FOUND") { @@ -308,8 +328,7 @@ function getNodeFiles(disableNodePathScan) { } else { result = false; } - log.debug("Module: "+mod.package.name+" "+mod.package.version+(result?"":" *ignored due to local copy*")); - log.debug(" "+mod.dir); + log.debug((result?"":"! ")+"Module: "+mod.package.name+" "+mod.package.version+" "+mod.dir+(result?"":" *ignored due to local copy*")); return result; }); diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index dbb6c6fc7..6e7609fd5 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -15,6 +15,7 @@ **/ const path = require("path"); +const semver = require("semver"); const {events,i18n,log} = require("@node-red/util"); var runtime; @@ -104,9 +105,78 @@ function createNodeApi(node) { return red; } + +function checkAgainstList(module,version,list) { + for (let i=0;i deniedRule.wildcardPos + } else { + // First wildcard in same position. + // Go with the longer matching rule. This isn't going to be 100% + // right, but we are deep into edge cases at this point. + return allowedRule.module.toString().length > deniedRule.module.toString().length + } + return false; +} + +function parseModuleList(list) { + list = list || ["*"]; + return list.map(rule => { + let m = /^(.+?)(?:@(.*))?$/.exec(rule); + let wildcardPos = m[1].indexOf("*"); + wildcardPos = wildcardPos===-1?Infinity:wildcardPos; + + return { + module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), + version: m[2], + wildcardPos: wildcardPos + } + }) +} + + module.exports = { init: function(_runtime) { runtime = _runtime; }, - createNodeApi: createNodeApi + createNodeApi: createNodeApi, + parseModuleList: parseModuleList, + checkModuleAllowed: checkModuleAllowed } diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index 9ac83c814..dfaee4e54 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -164,7 +164,7 @@ var api = module.exports = { throw err; } if (opts.tarball) { - if (runtime.settings.editorTheme && runtime.settings.editorTheme.palette && runtime.settings.editorTheme.palette.upload === false) { + if (runtime.settings.externalModules && runtime.settings.externalModules.palette && runtime.settings.externalModules.palette.upload === false) { runtime.log.audit({event: "nodes.install",tarball:opts.tarball.file,error:"invalid_request"}, opts.req); var err = new Error("Invalid request"); err.code = "invalid_request"; @@ -406,10 +406,12 @@ var api = module.exports = { var lang = opts.lang; var prevLang = runtime.i18n.i.language; // Trigger a load from disk of the language if it is not the default - return runtime.i18n.i.changeLanguage(lang, function(){ - var catalog = runtime.i18n.i.getResourceBundle(lang, namespace); - runtime.i18n.i.changeLanguage(prevLang); - return catalog||{}; + return new Promise(resolve => { + runtime.i18n.i.changeLanguage(lang, function() { + var catalog = runtime.i18n.i.getResourceBundle(lang, namespace); + runtime.i18n.i.changeLanguage(prevLang); + resolve(catalog||{}); + }); }); }, diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 067a8e83a..d96ec5b58 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -90,10 +90,28 @@ var api = module.exports = { safeSettings.flowFilePretty = runtime.settings.flowFilePretty; } + if (runtime.settings.editorTheme && runtime.settings.editorTheme.palette) { + if (runtime.settings.editorTheme.palette.upload === false || runtime.settings.editorTheme.palette.editable === false) { + safeSettings.externalModules = {palette: { } } + } + if (runtime.settings.editorTheme.palette.upload === false) { + safeSettings.externalModules.palette.allowUpload = false; + } + if (runtime.settings.editorTheme.palette.editable === false) { + safeSettings.externalModules.palette.allowInstall = false; + safeSettings.externalModules.palette.allowUpload = false; + } + } + + if (runtime.settings.externalModules) { + safeSettings.externalModules = extend(safeSettings.externalModules||{},runtime.settings.externalModules); + } + if (!runtime.nodes.installerEnabled()) { - safeSettings.editorTheme = safeSettings.editorTheme || {}; - safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; - safeSettings.editorTheme.palette.editable = false; + safeSettings.externalModules = safeSettings.externalModules || {}; + safeSettings.externalModules.palette = safeSettings.externalModules.palette || {}; + safeSettings.externalModules.palette.allowInstall = false; + safeSettings.externalModules.palette.allowUpload = false; } if (runtime.storage.projects) { var activeProject = runtime.storage.projects.getActiveProject(); diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index dd1594467..40aea7e36 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -123,7 +123,15 @@ function start() { } log.info(os.type()+" "+os.release()+" "+os.arch()+" "+os.endianness()); return redNodes.load().then(function() { - + let autoInstallModules = false; + if (settings.hasOwnProperty('autoInstallModules')) { + log.warn(log._("server.deprecatedOption",{old:"autoInstallModules", new:"externalModules.autoInstall"})); + autoInstallModules = true; + } + if (settings.externalModules) { + // autoInstallModules = autoInstall enabled && (no palette setting || palette install not disabled) + autoInstallModules = settings.externalModules.autoInstall && (!settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) ; + } var i; var nodeErrors = redNodes.getNodeList(function(n) { return n.err!=null;}); var nodeMissing = redNodes.getNodeList(function(n) { return n.module && n.enabled && !n.loaded && !n.err;}); @@ -156,12 +164,12 @@ function start() { for (i in missingModules) { if (missingModules.hasOwnProperty(i)) { log.warn(" - "+i+" ("+missingModules[i].version+"): "+missingModules[i].types.join(", ")); - if (settings.autoInstallModules && i != "node-red") { + if (autoInstallModules && i != "node-red") { installingModules.push({id:i,version:missingModules[i].version}); } } } - if (!settings.autoInstallModules) { + if (!autoInstallModules) { log.info(log._("server.removing-modules")); redNodes.cleanModuleList(); } else if (installingModules.length > 0) { @@ -186,11 +194,18 @@ function start() { var reinstallAttempts = 0; var reinstallTimeout; function reinstallModules(moduleList) { - var promises = []; - var reinstallList = []; - + const promises = []; + const reinstallList = []; + const installRetry = 30000; + if (settings.hasOwnProperty('autoInstallModulesRetry')) { + log.warn(log._("server.deprecatedOption",{old:"autoInstallModulesRetry", new:"externalModules.autoInstallRetry"})); + installRetry = settings.autoInstallModulesRetry; + } + if (settings.externalModules && settings.externalModules.hasOwnProperty('autoInstallRetry')) { + installRetry = settings.externalModules.autoInstallRetry * 1000; + } for (var i=0;i { events.emit("runtime-event",{id:"node/added",retain:false,payload:m.nodes}); @@ -204,7 +219,7 @@ function reinstallModules(moduleList) { if (reinstallList.length > 0) { reinstallAttempts++; // First 5 at 1x timeout, next 5 at 2x, next 5 at 4x, then 8x - var timeout = (settings.autoInstallModulesRetry||30000) * Math.pow(2,Math.min(Math.floor(reinstallAttempts/5),3)); + var timeout = installRetry * Math.pow(2,Math.min(Math.floor(reinstallAttempts/5),3)); reinstallTimeout = setTimeout(function() { reinstallModules(reinstallList); },timeout); diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index bc866e09e..58eb7308d 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -42,6 +42,7 @@ "uninstall-failed-long": "Uninstall of module __name__ failed:", "uninstalled": "Uninstalled module: __name__" }, + "deprecatedOption": "Use of __old__ is deprecated. Use __new__ instead", "unable-to-listen": "Unable to listen on __listenpath__", "port-in-use": "Error: port in use", "uncaught-exception": "Uncaught Exception:", diff --git a/test/unit/@node-red/registry/lib/util_spec.js b/test/unit/@node-red/registry/lib/util_spec.js index a82519d11..d2384e5dc 100644 --- a/test/unit/@node-red/registry/lib/util_spec.js +++ b/test/unit/@node-red/registry/lib/util_spec.js @@ -14,7 +14,54 @@ * limitations under the License. **/ +const should = require("should"); +const NR_TEST_UTILS = require("nr-test-utils"); +const registryUtil = NR_TEST_UTILS.require("@node-red/registry/lib/util"); + describe("red/nodes/registry/util",function() { - it.skip("NEEDS TESTS"); + describe("createNodeApi", function() { + it.skip("needs tests"); + }); + describe("checkModuleAllowed", function() { + function checkList(module, version, allowList, denyList) { + return registryUtil.checkModuleAllowed( + module, + version, + registryUtil.parseModuleList(allowList), + registryUtil.parseModuleList(denyList) + ) + } + + it("allows module with no allow/deny list provided", function() { + checkList("abc","1.2.3",[],[]).should.be.true(); + }) + it("defaults allow to * when only deny list is provided", function() { + checkList("abc","1.2.3",["*"],["def"]).should.be.true(); + checkList("def","1.2.3",["*"],["def"]).should.be.false(); + }) + it("uses most specific matching rule", function() { + checkList("abc","1.2.3",["ab*"],["a*"]).should.be.true(); + checkList("def","1.2.3",["d*"],["de*"]).should.be.false(); + }) + it("checks version string using semver rules", function() { + // Deny + checkList("abc","1.2.3",["abc@1.2.2"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@1.2.4"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@>1.2.3"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@>=1.2.3"],["abc"]).should.be.false(); + + + checkList("node-red-contrib-foo","1.2.3",["*"],["*contrib*"]).should.be.false(); + + + // Allow + checkList("abc","1.2.3",["abc@1.2.3"],["*"]).should.be.true(); + checkList("abc","1.2.3",["abc@<1.2.4"],["*"]).should.be.true(); + checkList("abc","1.2.3",["abc"],["abc@>1.2.3"]).should.be.true(); + checkList("abc","1.2.3",["abc"],["abc@<1.2.3||>1.2.3"]).should.be.true(); + checkList("node-red-contrib-foo","1.2.3",["*contrib*"],["*"]).should.be.true(); + }) + + }) }); diff --git a/test/unit/@node-red/runtime/lib/api/settings_spec.js b/test/unit/@node-red/runtime/lib/api/settings_spec.js index d01235604..b5b4878e2 100644 --- a/test/unit/@node-red/runtime/lib/api/settings_spec.js +++ b/test/unit/@node-red/runtime/lib/api/settings_spec.js @@ -61,8 +61,8 @@ describe("runtime-api/settings", function() { result.should.not.have.property("foo",123); result.should.have.property("flowEncryptionType","test-key-type"); result.should.not.have.property("user"); - result.should.have.property("editorTheme"); - result.editorTheme.should.eql({palette:{editable:false}}); + result.should.have.property("externalModules"); + result.externalModules.should.eql({palette:{allowInstall:false, allowUpload: false}}); }) });