mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #2797 from node-red/externalModules
Add support for settings.externalModules
This commit is contained in:
commit
87c9ed6356
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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<list.length;i++) {
|
||||
var rule = list[i];
|
||||
if (rule.module.test(module)) {
|
||||
// Without a full semver library in the editor,
|
||||
// we skip the version check.
|
||||
// Not ideal - but will get caught in the runtime
|
||||
// if the user tries to install.
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||
if (!allowList && !denyList) {
|
||||
// Default to allow
|
||||
return true;
|
||||
}
|
||||
if (allowList.length === 0 && denyList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var allowedRule = checkAgainstList(module,version,allowList);
|
||||
var deniedRule = checkAgainstList(module,version,denyList);
|
||||
// console.log("A",allowedRule)
|
||||
// console.log("D",deniedRule)
|
||||
|
||||
if (allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (!allowedRule && deniedRule) {
|
||||
return false;
|
||||
}
|
||||
if (!allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
||||
return allowedRule.wildcardPos > 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 = $('<span class="button-group">').prependTo(toolBar);
|
||||
var uploadButton = $('<button type="button" class="red-ui-sidebar-header-button red-ui-palette-editor-upload-button"><label><i class="fa fa-upload"></i><form id="red-ui-palette-editor-upload-form" enctype="multipart/form-data"><input name="tarball" type="file" accept=".tgz"></label></button>').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;
|
||||
}
|
||||
|
@ -465,7 +465,7 @@ RED.projects.settings = (function() {
|
||||
metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
|
||||
var buttons = $('<div class="red-ui-palette-module-button-group"></div>').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) {
|
||||
$('<a href="#" class="red-ui-button red-ui-button-small">' + RED._("sidebar.project.projectSettings.install") + '</a>').appendTo(buttons)
|
||||
.on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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<list.length;i++) {
|
||||
let rule = list[i];
|
||||
if (rule.module.test(module)) {
|
||||
if (version && rule.version) {
|
||||
if (semver.satisfies(version,rule.version)) {
|
||||
return rule;
|
||||
}
|
||||
} else {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||
// console.log("checkModuleAllowed",module,version);//,allowList,denyList)
|
||||
if (!allowList && !denyList) {
|
||||
// Default to allow
|
||||
return true;
|
||||
}
|
||||
if (allowList.length === 0 && denyList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var allowedRule = checkAgainstList(module,version,allowList);
|
||||
var deniedRule = checkAgainstList(module,version,denyList);
|
||||
// console.log("A",allowedRule)
|
||||
// console.log("D",deniedRule)
|
||||
|
||||
if (allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (!allowedRule && deniedRule) {
|
||||
return false;
|
||||
}
|
||||
if (!allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
||||
return allowedRule.wildcardPos > 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
|
||||
}
|
||||
|
@ -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||{});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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<moduleList.length;i++) {
|
||||
if (settings.autoInstallModules && moduleList[i].id != "node-red") {
|
||||
if (moduleList[i].id != "node-red") {
|
||||
(function(mod) {
|
||||
promises.push(redNodes.installModule(mod.id,mod.version).then(m => {
|
||||
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);
|
||||
|
@ -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:",
|
||||
|
@ -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();
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -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}});
|
||||
|
||||
})
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user