// This module handles the management of modules required by the runtime and flows. // Essentially this means keeping track of what extra modules a flow requires, // ensuring those modules are installed and providing a standard way for nodes // to require those modules safely. const fs = require("fs-extra"); const registryUtil = require("./util"); const path = require("path"); const clone = require("clone"); const exec = require("@node-red/util").exec; const log = require("@node-red/util").log; const BUILTIN_MODULES = require('module').builtinModules; const EXTERNAL_MODULES_DIR = "externalModules"; // TODO: outsource running npm to a plugin const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm"; let registeredTypes = {}; let subflowTypes = {}; let settings; let knownExternalModules = {}; let installEnabled = true; let installAllowList = ['*']; let installDenyList = []; function getInstallDir() { return path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "externalModules")); } async function refreshExternalModules() { const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR)); try { const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8")); knownExternalModules = pkgFile.dependencies; } catch(err) { } } function init(_settings) { settings = _settings; knownExternalModules = {}; installEnabled = true; if (settings.externalModules && settings.externalModules.modules) { if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) { installAllowList = settings.externalModules.modules.allowList; installDenyList = settings.externalModules.modules.denyList; } if (settings.externalModules.modules.hasOwnProperty("allowInstall")) { installEnabled = settings.externalModules.modules.allowInstall } } installAllowList = registryUtil.parseModuleList(installAllowList); installDenyList = registryUtil.parseModuleList(installDenyList); } function register(type, dynamicModuleListProperty) { registeredTypes[type] = dynamicModuleListProperty; } function registerSubflow(type, subflowConfig) { subflowTypes[type] = subflowConfig; } function requireModule(module) { if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) { const e = new Error("Module not allowed"); e.code = "module_not_allowed"; throw e; } if (BUILTIN_MODULES.indexOf(module) !== -1) { return require(module); } if (!knownExternalModules[module]) { const e = new Error("Module not allowed"); e.code = "module_not_allowed"; throw e; } const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR)); const moduleDir = path.join(externalModuleDir,"node_modules",module); return require(moduleDir); } function parseModuleName(module) { var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module); if (match) { return { spec: module, module: match[1], version: match[2], builtin: BUILTIN_MODULES.indexOf(match[1]) !== -1, known: !!knownExternalModules[match[1]] } } return null; } function isInstalled(moduleDetails) { return moduleDetails.builtin || moduleDetails.known; } async function checkFlowDependencies(flowConfig) { let nodes = clone(flowConfig); await refreshExternalModules(); const checkedModules = {}; const promises = []; const errors = []; const checkedSubflows = {}; while (nodes.length > 0) { let n = nodes.shift(); if (subflowTypes[n.type] && !checkedSubflows[n.type]) { checkedSubflows[n.type] = true; nodes = nodes.concat(subflowTypes[n.type].flow) } else if (registeredTypes[n.type]) { let nodeModules = n[registeredTypes[n.type]] || []; if (!Array.isArray(nodeModules)) { nodeModules = [nodeModules] } nodeModules.forEach(module => { if (typeof module !== 'string') { module = module.module || ""; } if (module) { let moduleDetails = parseModuleName(module) if (moduleDetails && checkedModules[moduleDetails.module] === undefined) { checkedModules[moduleDetails.module] = isInstalled(moduleDetails) if (!checkedModules[moduleDetails.module]) { if (installEnabled) { promises.push(installModule(moduleDetails).catch(err => { errors.push({module: moduleDetails,error:err}); })) } else if (!installEnabled) { const e = new Error("Module install disabled - externalModules.modules.allowInstall=false"); e.code = "install_not_allowed"; errors.push({module: moduleDetails,error:e}); } } else if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { const e = new Error("Module not allowed"); e.code = "module_not_allowed"; errors.push({module: moduleDetails,error:e}); } } } }) } } return Promise.all(promises).then(refreshExternalModules).then(() => { if (errors.length > 0) { throw errors; } }) } async function ensureModuleDir() { const installDir = getInstallDir(); if (!fs.existsSync(installDir)) { await fs.ensureDir(installDir); } const pkgFile = path.join(installDir,"package.json"); if (!fs.existsSync(pkgFile)) { await fs.writeFile(path.join(installDir,"package.json"),`{ "name": "Node-RED-External-Modules", "description": "These modules are automatically installed by Node-RED to use in Function nodes.", "version": "1.0.0", "private": true, "dependencies": {} }`) } } async function installModule(moduleDetails) { let installSpec = moduleDetails.module; if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { const e = new Error("Install not allowed"); e.code = "install_not_allowed"; throw e; } if (moduleDetails.version) { installSpec = installSpec+"@"+moduleDetails.version; } log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"})); const installDir = getInstallDir(); await ensureModuleDir(); var args = ["install", installSpec, "--production"]; return exec.run(NPM_COMMAND, args, { cwd: installDir },true).then(result => { log.info("successfully installed: "+installSpec); }).catch(result => { var output = result.stderr; var e; if (/E404/.test(output) || /ETARGET/.test(output)) { log.error(log._("server.install.install-failed-not-found",{name:installSpec})); e = new Error("Module not found"); e.code = 404; throw e; } else { log.error(log._("server.install.install-failed-long",{name:installSpec})); log.error("------------------------------------------"); log.error(output); log.error("------------------------------------------"); e = new Error(log._("server.install.install-failed")); e.code = "unexpected_error"; throw e; } }) } module.exports = { init: init, register: register, registerSubflow: registerSubflow, checkFlowDependencies: checkFlowDependencies, require: requireModule }