From e8e8f70c27c17becc690774bdb4c73b021035dad Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 15 Apr 2018 11:18:10 +0100 Subject: [PATCH] WIP: create new runtime-api --- jsdoc.json | 17 + package.json | 226 +++++------ red/api/admin/flow.js | 88 ++-- red/api/admin/flows.js | 92 ++--- red/api/admin/index.js | 8 +- red/api/admin/nodes.js | 255 ++++-------- red/api/editor/credentials.js | 42 +- red/api/editor/index.js | 17 +- red/api/editor/settings.js | 129 ++---- red/api/editor/sshkeys.js | 101 ++--- red/api/editor/theme.js | 8 +- red/api/index.js | 9 +- red/api/util.js | 10 + red/red.js | 7 +- red/runtime-api/auth.js | 19 + red/runtime-api/comms.js | 19 + red/runtime-api/flows.js | 248 ++++++++++++ red/runtime-api/index.js | 64 +++ red/runtime-api/library.js | 35 ++ red/runtime-api/nodes.js | 377 ++++++++++++++++++ red/runtime-api/settings.js | 257 ++++++++++++ .../localfilesystem/projects/ssh/index.js | 5 + 22 files changed, 1422 insertions(+), 611 deletions(-) create mode 100644 jsdoc.json create mode 100644 red/runtime-api/auth.js create mode 100644 red/runtime-api/comms.js create mode 100644 red/runtime-api/flows.js create mode 100644 red/runtime-api/index.js create mode 100644 red/runtime-api/library.js create mode 100644 red/runtime-api/nodes.js create mode 100644 red/runtime-api/settings.js diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 000000000..ca6d6cdb1 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,17 @@ +{ + "tags": { + "allowUnknownTags": false, + "dictionaries": ["jsdoc"] + }, + "source": { + "include": [ + "./red/runtime-api" + ] + }, + "templates": { + "systemName": "Node-RED Runtime API", + "theme":"yeti", + "footer": "", + "copyright": "Released under the Apache License v2.0" + } +} diff --git a/package.json b/package.json index 07ec5e84d..5e4eaac68 100644 --- a/package.json +++ b/package.json @@ -1,115 +1,119 @@ { - "name": "node-red", - "version": "0.18.4", - "description": "A visual tool for wiring the Internet of Things", - "homepage": "http://nodered.org", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/node-red/node-red.git" - }, - "main": "red/red.js", - "scripts": { - "start": "node red.js", - "test": "grunt", - "build": "grunt build" - }, - "bin": { - "node-red": "./red.js", - "node-red-pi": "bin/node-red-pi" - }, - "contributors": [ - { - "name": "Nick O'Leary" + "name": "node-red", + "version": "0.18.4", + "description": "A visual tool for wiring the Internet of Things", + "homepage": "http://nodered.org", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red.git" }, - { - "name": "Dave Conway-Jones" + "main": "red/red.js", + "scripts": { + "start": "node red.js", + "test": "grunt", + "build": "grunt build", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json -t ./node_modules/ink-docstrap/template" + }, + "bin": { + "node-red": "./red.js", + "node-red-pi": "bin/node-red-pi" + }, + "contributors": [ + { + "name": "Nick O'Leary" + }, + { + "name": "Dave Conway-Jones" + } + ], + "keywords": [ + "editor", + "messaging", + "iot", + "flow" + ], + "dependencies": { + "basic-auth": "2.0.0", + "bcryptjs": "2.4.3", + "body-parser": "1.18.2", + "cheerio": "0.22.0", + "clone": "2.1.1", + "cookie": "0.3.1", + "cookie-parser": "1.4.3", + "cors": "2.8.4", + "cron": "1.3.0", + "express": "4.16.2", + "express-session": "1.15.6", + "follow-redirects": "1.3.0", + "fs-extra": "5.0.0", + "fs.notify": "0.0.4", + "hash-sum": "1.0.2", + "i18next": "1.10.6", + "ink-docstrap": "^1.3.2", + "is-utf8": "0.2.1", + "js-yaml": "3.10.0", + "json-stringify-safe": "5.0.1", + "jsonata": "1.5.0", + "media-typer": "0.3.0", + "memorystore": "1.6.0", + "mime": "1.4.1", + "mqtt": "2.15.1", + "multer": "1.3.0", + "mustache": "2.3.0", + "node-red-node-email": "0.1.*", + "node-red-node-feedparser": "0.1.*", + "node-red-node-rbe": "0.2.*", + "node-red-node-twitter": "0.1.*", + "nopt": "4.0.1", + "oauth2orize": "1.11.0", + "on-headers": "1.0.1", + "passport": "0.4.0", + "passport-http-bearer": "1.0.1", + "passport-oauth2-client-password": "0.1.2", + "raw-body": "2.3.2", + "semver": "5.4.1", + "sentiment": "2.1.0", + "uglify-js": "3.3.6", + "when": "3.7.8", + "ws": "1.1.5", + "xml2js": "0.4.19" + }, + "optionalDependencies": { + "bcrypt": "~1.0.3" + }, + "devDependencies": { + "chromedriver": "^2.33.2", + "grunt": "~1.0.1", + "grunt-chmod": "~1.1.1", + "grunt-cli": "~1.2.0", + "grunt-concurrent": "~2.3.1", + "grunt-contrib-clean": "~1.1.0", + "grunt-contrib-compress": "~1.4.0", + "grunt-contrib-concat": "~1.0.1", + "grunt-contrib-copy": "~1.0.0", + "grunt-contrib-jshint": "~1.1.0", + "grunt-contrib-uglify": "~3.3.0", + "grunt-contrib-watch": "~1.0.0", + "grunt-jsonlint": "~1.1.0", + "grunt-mocha-istanbul": "5.0.2", + "grunt-nodemon": "~0.4.2", + "grunt-sass": "~2.0.0", + "grunt-simple-mocha": "~0.4.1", + "grunt-webdriver": "^2.0.3", + "istanbul": "0.4.5", + "jsdoc": "3.5.5", + "mocha": "^5.1.1", + "should": "^8.4.0", + "sinon": "1.17.7", + "stoppable": "^1.0.6", + "supertest": "3.0.0", + "wdio-chromedriver-service": "^0.1.1", + "wdio-mocha-framework": "^0.5.11", + "wdio-spec-reporter": "^0.1.3", + "webdriverio": "^4.9.11" + }, + "engines": { + "node": ">=4" } - ], - "keywords": [ - "editor", - "messaging", - "iot", - "flow" - ], - "dependencies": { - "basic-auth": "2.0.0", - "bcryptjs": "2.4.3", - "body-parser": "1.18.2", - "cheerio": "0.22.0", - "clone": "2.1.1", - "cookie": "0.3.1", - "cookie-parser": "1.4.3", - "cors": "2.8.4", - "cron": "1.3.0", - "express": "4.16.2", - "express-session": "1.15.6", - "follow-redirects": "1.3.0", - "fs-extra": "5.0.0", - "fs.notify": "0.0.4", - "hash-sum": "1.0.2", - "i18next": "1.10.6", - "is-utf8": "0.2.1", - "js-yaml": "3.10.0", - "json-stringify-safe": "5.0.1", - "jsonata": "1.5.0", - "media-typer": "0.3.0", - "memorystore": "1.6.0", - "mqtt": "2.15.1", - "multer": "1.3.0", - "mustache": "2.3.0", - "node-red-node-email": "0.1.*", - "node-red-node-feedparser": "0.1.*", - "node-red-node-rbe": "0.2.*", - "node-red-node-twitter": "0.1.*", - "nopt": "4.0.1", - "oauth2orize": "1.11.0", - "on-headers": "1.0.1", - "passport": "0.4.0", - "passport-http-bearer": "1.0.1", - "passport-oauth2-client-password": "0.1.2", - "raw-body": "2.3.2", - "semver": "5.4.1", - "sentiment": "2.1.0", - "uglify-js": "3.3.6", - "when": "3.7.8", - "ws": "1.1.5", - "xml2js": "0.4.19" - }, - "optionalDependencies": { - "bcrypt": "~1.0.3" - }, - "devDependencies": { - "chromedriver": "^2.33.2", - "grunt": "~1.0.1", - "grunt-chmod": "~1.1.1", - "grunt-cli": "~1.2.0", - "grunt-concurrent": "~2.3.1", - "grunt-contrib-clean": "~1.1.0", - "grunt-contrib-compress": "~1.4.0", - "grunt-contrib-concat": "~1.0.1", - "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "~1.1.0", - "grunt-contrib-uglify": "~3.3.0", - "grunt-contrib-watch": "~1.0.0", - "grunt-jsonlint": "~1.1.0", - "grunt-mocha-istanbul": "5.0.2", - "grunt-nodemon": "~0.4.2", - "grunt-sass": "~2.0.0", - "grunt-simple-mocha": "~0.4.1", - "grunt-webdriver": "^2.0.3", - "istanbul": "0.4.5", - "mocha": "^5.1.1", - "should": "^8.4.0", - "sinon": "1.17.7", - "stoppable": "^1.0.6", - "supertest": "3.0.0", - "wdio-chromedriver-service": "^0.1.1", - "wdio-mocha-framework": "^0.5.11", - "wdio-spec-reporter": "^0.1.3", - "webdriverio": "^4.9.11" - }, - "engines": { - "node": ">=4" - } } diff --git a/red/api/admin/flow.js b/red/api/admin/flow.js index df1cf2d0c..5ba5d7a04 100644 --- a/red/api/admin/flow.js +++ b/red/api/admin/flow.js @@ -14,72 +14,56 @@ * limitations under the License. **/ -var log; -var redNodes; +var runtimeAPI; +var apiUtils = require("../util"); module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, get: function(req,res) { - var id = req.params.id; - var flow = redNodes.getFlow(id); - if (flow) { - log.audit({event: "flow.get",id:id},req); - res.json(flow); - } else { - log.audit({event: "flow.get",id:id,error:"not_found"},req); - res.status(404).end(); + var opts = { + user: req.user, + id: req.params.id } + runtimeAPI.flows.getFlow(opts).then(function(result) { + return res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, post: function(req,res) { - var flow = req.body; - redNodes.addFlow(flow).then(function(id) { - log.audit({event: "flow.add",id:id},req); - res.json({id:id}); + var opts = { + user: req.user, + flow: req.body + } + runtimeAPI.flows.addFlow(opts).then(function(id) { + return res.json({id:id}); }).catch(function(err) { - log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + apiUtils.rejectHandler(req,res,err); }) - }, put: function(req,res) { - var id = req.params.id; - var flow = req.body; - try { - redNodes.updateFlow(id,flow).then(function() { - log.audit({event: "flow.update",id:id},req); - res.json({id:id}); - }).catch(function(err) { - log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }) - } catch(err) { - if (err.code === 404) { - log.audit({event: "flow.update",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id, + flow: req.body } + runtimeAPI.flows.updateFlow(opts).then(function(id) { + return res.json({id:id}); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, delete: function(req,res) { - var id = req.params.id; - try { - redNodes.removeFlow(id).then(function() { - log.audit({event: "flow.remove",id:id},req); - res.status(204).end(); - }) - } catch(err) { - if (err.code === 404) { - log.audit({event: "flow.remove",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id } + runtimeAPI.flows.deleteFlow(opts).then(function() { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/admin/flows.js b/red/api/admin/flows.js index d3eceecb5..35b234dfa 100644 --- a/red/api/admin/flows.js +++ b/red/api/admin/flows.js @@ -14,72 +14,56 @@ * limitations under the License. **/ -var log; -var redNodes; +var runtimeAPI; module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, get: function(req,res) { var version = req.get("Node-RED-API-Version")||"v1"; - if (version === "v1") { - log.audit({event: "flows.get",version:"v1"},req); - res.json(redNodes.getFlows().flows); - } else if (version === "v2") { - log.audit({event: "flows.get",version:"v2"},req); - res.json(redNodes.getFlows()); - } else { - log.audit({event: "flows.get",version:version,error:"invalid_api_version"},req); - res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"}); + if (!/^v[12]$/.test(version)) { + return res.status(500).json({code:"invalid_api_version", message:"Invalid API Version requested"}); } + var opts = { + user: req.user + } + runtimeAPI.flows.getFlows(opts).then(function(result) { + if (version === "v1") { + res.json(result.flows); + } else if (version === "v2") { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, post: function(req,res) { var version = req.get("Node-RED-API-Version")||"v1"; if (!/^v[12]$/.test(version)) { - log.audit({event: "flows.set",version:version,error:"invalid_api_version"},req); - res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"}); - return; + return res.status(500).json({code:"invalid_api_version", message:"Invalid API Version requested"}); } - var flows = req.body; - var deploymentType = req.get("Node-RED-Deployment-Type")||"full"; - log.audit({event: "flows.set",type:deploymentType,version:version},req); - if (deploymentType === 'reload') { - redNodes.loadFlows().then(function(flowId) { - if (version === "v1") { - res.status(204).end(); - } else { - res.json({rev:flowId}); - } - }).catch(function(err) { - log.warn(log._("api.flows.error-reload",{message:err.message})); - log.warn(err.stack); - res.status(500).json({error:"unexpected_error", message:err.message}); - }); - } else { - var flowConfig = flows; - if (version === "v2") { - flowConfig = flows.flows; - if (flows.hasOwnProperty('rev')) { - var currentVersion = redNodes.getFlows().rev; - if (currentVersion !== flows.rev) { - //TODO: log warning - return res.status(409).json({code:"version_mismatch"}); - } - } + var opts = { + user: req.user, + deploymentType: req.get("Node-RED-Deployment-Type")||"full" + } + + if (opts.deploymentType !== 'reload') { + if (version === "v1") { + opts.flows = {flows: req.body} + } else { + opts.flows = req.body; } - redNodes.setFlows(flowConfig,deploymentType).then(function(flowId) { - if (version === "v1") { - res.status(204).end(); - } else if (version === "v2") { - res.json({rev:flowId}); - } - }).catch(function(err) { - log.warn(log._("api.flows.error-save",{message:err.message})); - log.warn(err.stack); - res.status(500).json({error:err.code || "unexpected_error", message:err.message}); - }); } + + runtimeAPI.flows.setFlows(opts).then(function(result) { + if (version === "v1") { + res.status(204).end(); + } else { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/admin/index.js b/red/api/admin/index.js index 57c79eb62..9679eaefc 100644 --- a/red/api/admin/index.js +++ b/red/api/admin/index.js @@ -24,10 +24,10 @@ var auth = require("../auth"); var apiUtil = require("../util"); module.exports = { - init: function(runtime) { - flows.init(runtime); - flow.init(runtime); - nodes.init(runtime); + init: function(runtimeAPI) { + flows.init(runtimeAPI); + flow.init(runtimeAPI); + nodes.init(runtimeAPI); var needsPermission = auth.needsPermission; diff --git a/red/api/admin/nodes.js b/red/api/admin/nodes.js index 032623bb4..6dbe1cc05 100644 --- a/red/api/admin/nodes.js +++ b/red/api/admin/nodes.js @@ -14,230 +14,133 @@ * limitations under the License. **/ -var when = require("when"); var apiUtils = require("../util"); -var redNodes; -var log; -var settings; + +var runtimeAPI; module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; - settings = runtime.settings; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, getAll: function(req,res) { + var opts = { + user: req.user + } if (req.get("accept") == "application/json") { - log.audit({event: "nodes.list.get"},req); - res.json(redNodes.getNodeList()); + runtimeAPI.nodes.getNodeList(opts).then(function(list) { + res.json(list); + }) } else { - var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); - log.audit({event: "nodes.configs.get"},req); - res.send(redNodes.getNodeConfigs(lang)); + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.nodes.getNodeConfigs(opts).then(function(configs) { + res.send(configs); + }) } }, post: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.install",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; + var opts = { + user: req.user, + module: req.body.module, + version: req.body.version } - var node = req.body; - var promise; - var isUpgrade = false; - if (node.module) { - var module = redNodes.getModuleInfo(node.module); - if (module) { - if (!node.version || module.version === node.version) { - log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_already_loaded"},req); - res.status(400).json({error:"module_already_loaded", message:"Module already loaded"}); - return; - } - if (!module.local) { - log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_not_local"},req); - res.status(400).json({error:"module_not_local", message:"Module not locally installed"}); - return; - } - isUpgrade = true; - } - promise = redNodes.installModule(node.module,node.version); - } else { - log.audit({event: "nodes.install",module:node.module,error:"invalid_request"},req); - res.status(400).json({error:"invalid_request", message:"Invalid request"}); - return; - } - promise.then(function(info) { - if (node.module) { - log.audit({event: "nodes.install",module:node.module,version:node.version},req); - res.json(info); - } + runtimeAPI.nodes.addModule(opts).then(function(info) { + res.json(info); }).catch(function(err) { - if (err.code === 404) { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:"not_found"},req); - res.status(404).end(); - } else if (err.code) { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code},req); - res.status(400).json({error:err.code, message:err.message}); - } else { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } - }); + apiUtils.rejectHandler(req,res,err); + }) }, delete: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.remove",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } - var mod = req.params[0]; - try { - var promise = null; - var module = redNodes.getModuleInfo(mod); - if (!module) { - log.audit({event: "nodes.remove",module:mod,error:"not_found"},req); - res.status(404).end(); - return; - } else { - promise = redNodes.uninstallModule(mod); - } - - promise.then(function(list) { - log.audit({event: "nodes.remove",module:mod},req); - res.status(204).end(); - }).catch(function(err) { - log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }); - } catch(err) { - log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + module: req.params[0] } + runtimeAPI.nodes.removeModule(opts).then(function(info) { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, getSet: function(req,res) { - var id = req.params[0] + "/" + req.params[2]; - var result = null; + var opts = { + user: req.user, + id: req.params[0] + "/" + req.params[2] + } if (req.get("accept") === "application/json") { - result = redNodes.getNodeInfo(id); - if (result) { - log.audit({event: "nodes.info.get",id:id},req); - delete result.loaded; + runtimeAPI.nodes.getNodeInfo(opts).then(function(result) { res.send(result); - } else { - log.audit({event: "nodes.info.get",id:id,error:"not_found"},req); - res.status(404).end(); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } else { - var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); - result = redNodes.getNodeConfig(id,lang); - if (result) { - log.audit({event: "nodes.config.get",id:id},req); - res.send(result); - } else { - log.audit({event: "nodes.config.get",id:id,error:"not_found"},req); - res.status(404).end(); - } + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.nodes.getNodeConfig(opts).then(function(result) { + return res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } }, getModule: function(req,res) { - var module = req.params[0]; - var result = redNodes.getModuleInfo(module); - if (result) { - log.audit({event: "nodes.module.get",module:module},req); - res.json(result); - } else { - log.audit({event: "nodes.module.get",module:module,error:"not_found"},req); - res.status(404).end(); + var opts = { + user: req.user, + module: req.params[0] } + runtimeAPI.nodes.getModuleInfo(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, putSet: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.info.set",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } var body = req.body; if (!body.hasOwnProperty("enabled")) { - log.audit({event: "nodes.info.set",error:"invalid_request"},req); + // log.audit({event: "nodes.module.set",error:"invalid_request"},req); res.status(400).json({error:"invalid_request", message:"Invalid request"}); return; } - var id = req.params[0] + "/" + req.params[2]; - try { - var node = redNodes.getNodeInfo(id); - var info; - if (!node) { - log.audit({event: "nodes.info.set",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - delete node.loaded; - putNode(node, body.enabled).then(function(result) { - log.audit({event: "nodes.info.set",id:id,enabled:body.enabled},req); - res.json(result); - }); - } - } catch(err) { - log.audit({event: "nodes.info.set",id:id,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + id: req.params[0] + "/" + req.params[2], + enabled: body.enabled } + runtimeAPI.nodes.setNodeSetState(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, putModule: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.module.set",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } var body = req.body; if (!body.hasOwnProperty("enabled")) { - log.audit({event: "nodes.module.set",error:"invalid_request"},req); + // log.audit({event: "nodes.module.set",error:"invalid_request"},req); res.status(400).json({error:"invalid_request", message:"Invalid request"}); return; } - var mod = req.params[0]; - try { - var module = redNodes.getModuleInfo(mod); - if (!module) { - log.audit({event: "nodes.module.set",module:mod,error:"not_found"},req); - return res.status(404).end(); - } - - var nodes = module.nodes; - var promises = []; - for (var i = 0; i < nodes.length; ++i) { - promises.push(putNode(nodes[i],body.enabled)); - } - when.settle(promises).then(function() { - res.json(redNodes.getModuleInfo(mod)); - }); - } catch(err) { - log.audit({event: "nodes.module.set",module:mod,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + module: req.params[0], + enabled: body.enabled } + runtimeAPI.nodes.setModuleState(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) + }, getIcons: function(req,res) { - log.audit({event: "nodes.icons.get"},req); - res.json(redNodes.getNodeIcons()); + var opts = { + user: req.user + } + runtimeAPI.nodes.getIconList(opts).then(function(list) { + res.json(list); + }); } }; - -function putNode(node, enabled) { - var info; - var promise; - if (!node.err && node.enabled === enabled) { - promise = when.resolve(node); - } else { - if (enabled) { - promise = redNodes.enableNode(node.id); - } else { - promise = redNodes.disableNode(node.id); - } - } - return promise; -} diff --git a/red/api/editor/credentials.js b/red/api/editor/credentials.js index ae7429bcd..4cd8d3c9b 100644 --- a/red/api/editor/credentials.js +++ b/red/api/editor/credentials.js @@ -14,39 +14,23 @@ * limitations under the License. **/ -var log; -var api; +var runtimeAPI; +var apiUtils = require("../util"); module.exports = { - init: function(runtime) { - log = runtime.log; - api = runtime.nodes; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI }, get: function (req, res) { - // TODO: It should verify the given node id is of the type specified - - // but that would add a dependency from this module to the - // registry module that knows about node types. - var nodeType = req.params.type; - var nodeID = req.params.id; - log.audit({event: "credentials.get",type:nodeType,id:nodeID},req); - var credentials = api.getCredentials(nodeID); - if (!credentials) { - res.json({}); - return; + var opts = { + user: req.user, + type: req.params.type, + id: req.params.id } - var definition = api.getCredentialDefinition(nodeType); - - var sendCredentials = {}; - for (var cred in definition) { - if (definition.hasOwnProperty(cred)) { - if (definition[cred].type == "password") { - var key = 'has_' + cred; - sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; - continue; - } - sendCredentials[cred] = credentials[cred] || ''; - } - } - res.json(sendCredentials); + runtimeAPI.flows.getNodeCredentials(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/editor/index.js b/red/api/editor/index.js index e27d03d6c..6412c2411 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -25,6 +25,7 @@ var auth = require("../auth"); var nodes = require("../admin/nodes"); // TODO: move /icons into here var needsPermission; var runtime; +var runtimeAPI; var log; var apiUtil = require("../util"); @@ -38,16 +39,17 @@ var ensureRuntimeStarted = function(req,res,next) { } module.exports = { - init: function(server, _runtime) { + init: function(server, settings, _runtime, _runtimeAPI) { runtime = _runtime; + runtimeAPI = _runtimeAPI; log = runtime.log; needsPermission = auth.needsPermission; - var settings = runtime.settings; if (!settings.disableEditor) { - info.init(runtime); + info.init(runtimeAPI); comms.init(server,runtime); var ui = require("./ui"); + // ui is passed runtime so it get access runtime.nodes.getNodeIconPath ui.init(runtime); var editorApp = express(); if (settings.requireHttps === true) { @@ -67,7 +69,7 @@ module.exports = { editorApp.get("/icons/:scope/:module/:icon",ui.icon); var theme = require("./theme"); - theme.init(runtime); + theme.init(settings, runtime.version()); editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); @@ -91,7 +93,7 @@ module.exports = { // Credentials var credentials = require("./credentials"); - credentials.init(runtime); + credentials.init(runtimeAPI); editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler); // Settings @@ -100,11 +102,8 @@ module.exports = { editorApp.get("/settings/user",needsPermission("settings.read"),info.userSettings,apiUtil.errorHandler); // User Settings editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler); - // SSH keys - var sshkeys = require("./sshkeys"); - sshkeys.init(runtime); - editorApp.use("/settings/user/keys",sshkeys.app()); + editorApp.use("/settings/user/keys",info.sshkeys()); return editorApp; } diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js index 8b45d7271..4c2ea5847 100644 --- a/red/api/editor/settings.js +++ b/red/api/editor/settings.js @@ -13,117 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var theme = require("../editor/theme"); -var util = require('util'); -var runtime; -var settings; -var log; +var apiUtils = require("../util"); +var runtimeAPI; +var sshkeys = require("./sshkeys"); +var theme = require("./theme"); module.exports = { - init: function(_runtime) { - runtime = _runtime; - settings = runtime.settings; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; + sshkeys.init(runtimeAPI); }, runtimeSettings: function(req,res) { - var safeSettings = { - httpNodeRoot: settings.httpNodeRoot||"/", - version: settings.version, + var opts = { user: req.user } - - var themeSettings = theme.settings(); - if (themeSettings) { - safeSettings.editorTheme = themeSettings; - } - - if (util.isArray(settings.paletteCategories)) { - safeSettings.paletteCategories = settings.paletteCategories; - } - - if (settings.flowFilePretty) { - safeSettings.flowFilePretty = settings.flowFilePretty; - } - - if (!runtime.nodes.paletteEditorEnabled()) { - safeSettings.editorTheme = safeSettings.editorTheme || {}; - safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; - safeSettings.editorTheme.palette.editable = false; - } - if (runtime.storage.projects) { - var activeProject = runtime.storage.projects.getActiveProject(); - if (activeProject) { - safeSettings.project = activeProject; - } else if (runtime.storage.projects.flowFileExists()) { - safeSettings.files = { - flow: runtime.storage.projects.getFlowFilename(), - credentials: runtime.storage.projects.getCredentialsFilename() - } + runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) { + var themeSettings = theme.settings(); + if (themeSettings) { + result.editorTheme = themeSettings; } - safeSettings.git = { - globalUser: runtime.storage.projects.getGlobalGitUser() - } - } - - safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); - - settings.exportNodeSettings(safeSettings); - res.json(safeSettings); + res.json(result); + }); }, userSettings: function(req, res) { - var username; - if (!req.user || req.user.anonymous) { - username = '_'; - } else { - username = req.user.username; + var opts = { + user: req.user } - res.json(settings.getUserSettings(username)||{}); + runtimeAPI.settings.getUserSettings(opts).then(function(result) { + res.json(result); + }); }, updateUserSettings: function(req,res) { - var username; - if (!req.user || req.user.anonymous) { - username = '_'; - } else { - username = req.user.username; - } - var currentSettings = settings.getUserSettings(username)||{}; - currentSettings = extend(currentSettings, req.body); - try { - settings.setUserSettings(username, currentSettings).then(function() { - log.audit({event: "settings.update",username:username},req); - res.status(204).end(); - }).catch(function(err) { - log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }); - } catch(err) { - log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")})); - log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + settings: req.body } + runtimeAPI.settings.updateUserSettings(opts).then(function(result) { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); + }, + sshkeys: function() { + return sshkeys.app() } } - -function extend(target, source) { - var keys = Object.keys(source); - var i = keys.length; - while(i--) { - var value = source[keys[i]] - var type = typeof value; - if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) { - target[keys[i]] = value; - } else if (value === null) { - if (target.hasOwnProperty(keys[i])) { - delete target[keys[i]]; - } - } else { - // Object - if (target.hasOwnProperty(keys[i])) { - target[keys[i]] = extend(target[keys[i]],value); - } else { - target[keys[i]] = value; - } - } - } - return target; -} diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index 6f34d2b2d..62ccdc574 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -15,8 +15,7 @@ **/ var express = require("express"); -var os = require("os"); -var runtime; +var runtimeAPI; var needsPermission = require("../auth").needsPermission; function getUsername(userObj) { @@ -28,94 +27,66 @@ function getUsername(userObj) { } module.exports = { - init: function(_runtime) { - runtime = _runtime; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, app: function() { var app = express(); - // SSH keys - // List all SSH keys app.get("/", needsPermission("settings.read"), function(req,res) { - var username = getUsername(req.user); - runtime.storage.projects.ssh.listSSHKeys(username) - .then(function(list) { + var opts = { + user: req.user + } + runtimeAPI.settings.getUserKeys(opts).then(function(list) { res.json({ keys: list }); - }) - .catch(function(err) { - // console.log(err.stack); - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); // Get SSH key detail app.get("/:id", needsPermission("settings.read"), function(req,res) { - var username = getUsername(req.user); - // console.log('username:', username); - runtime.storage.projects.ssh.getSSHKey(username, req.params.id) - .then(function(data) { - if (data) { - res.json({ - publickey: data - }); - } else { - res.status(404).end(); - } - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.getUserKey(opts).then(function(data) { + res.json({ + publickey: data + }); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); // Generate a SSH key app.post("/", needsPermission("settings.write"), function(req,res) { - var username = getUsername(req.user); - // console.log('req.body:', req.body); - if ( req.body && req.body.name && /^[a-zA-Z0-9\-_]+$/.test(req.body.name)) { - runtime.storage.projects.ssh.generateSSHKey(username, req.body) - .then(function(name) { - // console.log('generate key --- success name:', name); - res.json({ - name: name - }); - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.generateUserKey(opts).then(function(name) { + res.json({ + name: name }); - } - else { - res.status(400).json({error:"unexpected_error", message:"You need to have body or body.name"}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); }); // Delete a SSH key app.delete("/:id", needsPermission("settings.write"), function(req,res) { - var username = getUsername(req.user); - runtime.storage.projects.ssh.deleteSSHKey(username, req.params.id) - .then(function() { + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.generateUserKey(opts).then(function(name) { res.status(204).end(); - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); diff --git a/red/api/editor/theme.js b/red/api/editor/theme.js index 6699e7cd7..62e520579 100644 --- a/red/api/editor/theme.js +++ b/red/api/editor/theme.js @@ -40,7 +40,6 @@ var defaultContext = { var theme = null; var themeContext = clone(defaultContext); var themeSettings = null; -var runtime = null; var themeApp; @@ -78,11 +77,10 @@ function serveFilesFromTheme(themeValue, themeApp, directory) { } module.exports = { - init: function(runtime) { - var settings = runtime.settings; + init: function(settings,version) { themeContext = clone(defaultContext); - if (runtime.version) { - themeContext.version = runtime.version(); + if (version) { + themeContext.version = version; } themeSettings = null; theme = settings.editorTheme || {}; diff --git a/red/api/index.js b/red/api/index.js index dab88064c..7ee242ac9 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -26,13 +26,10 @@ var apiUtil = require("./util"); var adminApp; var server; -var runtime; var editor; -function init(_server,_runtime) { +function init(_server,settings,runtime,runtimeAPI) { server = _server; - runtime = _runtime; - var settings = runtime.settings; if (settings.httpAdminRoot !== false) { apiUtil.init(runtime); adminApp = express(); @@ -61,7 +58,7 @@ function init(_server,_runtime) { // Editor if (!settings.disableEditor) { editor = require("./editor"); - var editorApp = editor.init(server, runtime); + var editorApp = editor.init(server, settings, runtime, runtimeAPI); adminApp.use(editorApp); } @@ -70,7 +67,7 @@ function init(_server,_runtime) { adminApp.use(corsHandler); } - var adminApiApp = require("./admin").init(runtime); + var adminApiApp = require("./admin").init(runtimeAPI); adminApp.use(adminApiApp); } else { adminApp = null; diff --git a/red/api/util.js b/red/api/util.js index bb3e2be47..4a160a23d 100644 --- a/red/api/util.js +++ b/red/api/util.js @@ -41,5 +41,15 @@ module.exports = { lang = acceptedLanguages[0]; } return lang; + }, + rejectHandler: function(req,res,err) { + res.status(err.status||500); + if (err.code || err.message) { + res.json({ + code: err.code||"unexpected_error", + message: err.message + }) + } + res.end(); } } diff --git a/red/red.js b/red/red.js index 7aec5750a..15259330d 100644 --- a/red/red.js +++ b/red/red.js @@ -18,6 +18,8 @@ var fs = require("fs"); var path = require('path'); var runtime = require("./runtime"); +var runtimeAPI = require("./runtime-api"); + var api = require("./api"); process.env.NODE_RED_HOME = process.env.NODE_RED_HOME || path.resolve(__dirname+"/.."); @@ -67,7 +69,10 @@ module.exports = { if (userSettings.httpAdminRoot !== false) { runtime.init(userSettings,api); - api.init(httpServer,runtime); + + runtimeAPI.init(runtime); + api.init(httpServer,userSettings,runtime,runtimeAPI); + apiEnabled = true; server = runtime.adminApi.server; runtime.server = runtime.adminApi.server; diff --git a/red/runtime-api/auth.js b/red/runtime-api/auth.js new file mode 100644 index 000000000..420fef54b --- /dev/null +++ b/red/runtime-api/auth.js @@ -0,0 +1,19 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/auth + */ diff --git a/red/runtime-api/comms.js b/red/runtime-api/comms.js new file mode 100644 index 000000000..090a767bf --- /dev/null +++ b/red/runtime-api/comms.js @@ -0,0 +1,19 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/comms + */ diff --git a/red/runtime-api/flows.js b/red/runtime-api/flows.js new file mode 100644 index 000000000..f27ab4b34 --- /dev/null +++ b/red/runtime-api/flows.js @@ -0,0 +1,248 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + /** + * @namespace RED.flows + */ + +/** + * @typedef Flows + * @alias Dave + * @type {object} + * @property {string} rev - the flow revision identifier + * @property {Array} flows - the flow configuration, an array of node configuration objects + */ + +/** + * @typedef Flow + * @type {object} + * @property {string} id - the flow identifier + * @property {string} label - a label for the flow + * @property {Array} nodes - an array of node configuration objects + */ + +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + /** + * Gets the current flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + getFlows: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "flows.get"}/*,req*/); + var version = opts.version||"v1"; + return resolve(runtime.nodes.getFlows()); + }); + }, + /** + * Sets the current flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + setFlows: function(opts) { + var err; + return new Promise(function(resolve,reject) { + + var flows = opts.flows; + var deploymentType = opts.deploymentType||"full"; + runtime.log.audit({event: "flows.set",type:deploymentType}/*,req*/); + + var apiPromise; + if (deploymentType === 'reload') { + apiPromise = runtime.nodes.loadFlows(); + } else { + if (flows.hasOwnProperty('rev')) { + var currentVersion = runtime.nodes.getFlows().rev; + if (currentVersion !== flows.rev) { + err = new Error(); + err.code = "version_mismatch"; + err.status = 409; + //TODO: log warning + return reject(err); + } + } + apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType); + } + apiPromise.then(function(flowId) { + return resolve({rev:flowId}); + }).catch(function(err) { + log.warn(log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message})); + log.warn(err.stack); + return reject(err); + }); + }); + }, + + /** + * Adds a flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.flow - the flow to add + * @return {Promise} - the id of the added flow + * @memberof RED.flows + */ + addFlow: function(opts) { + return new Promise(function(resolve,reject) { + var flow = opts.flow; + runtime.nodes.addFlow(flow).then(function(id) { + runtime.log.audit({event: "flow.add",id:id}); + return resolve(id); + }).catch(function(err) { + runtime.log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }) + }) + + + }, + + /** + * Gets an individual flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to retrieve + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + getFlow: function(opts) { + return new Promise(function (resolve,reject) { + var flow = runtime.nodes.getFlow(opts.id); + if (flow) { + runtime.log.audit({event: "flow.get",id:opts.id}); + return resolve(flow); + } else { + runtime.log.audit({event: "flow.get",id:opts.id,error:"not_found"}); + var err = new Error(); + err.status = 404; + return reject(err); + } + }) + + }, + /** + * Updates an existing flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to update + * @param {Object} opts.flow - the flow configuration + * @return {Promise} - the id of the updated flow + * @memberof RED.flows + */ + updateFlow: function(opts) { + return new Promise(function (resolve,reject) { + var flow = opts.flow; + var id = opts.id; + try { + runtime.nodes.updateFlow(id,flow).then(function() { + runtime.log.audit({event: "flow.update",id:id}); + return resolve(id); + }).catch(function(err) { + runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }) + } catch(err) { + if (err.code === 404) { + runtime.log.audit({event: "flow.update",id:id,error:"not_found"}); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + } + }); + + }, + /** + * Deletes a flow + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to delete + * @return {Promise} - resolves if successful + * @memberof RED.flows + */ + deleteFlow: function(opts) { + return new Promise(function (resolve,reject) { + var id = opts.id; + try { + runtime.nodes.removeFlow(id).then(function() { + log.audit({event: "flow.remove",id:id}); + return resolve(); + }) + } catch(err) { + if (err.code === 404) { + log.audit({event: "flow.remove",id:id,error:"not_found"}); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + } + }); + }, + + /** + * Gets the safe credentials for a node + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the node type to return the credential information for + * @param {String} opts.id - the node id + * @return {Promise} - the safe credentials + * @memberof RED.flows + */ + getNodeCredentials: function(opts) { + return new Promise(function(resolve,reject) { + log.audit({event: "credentials.get",type:opts.type,id:opts.id}); + var credentials = runtime.nodes.getCredentials(opts.id); + if (!credentials) { + return resolve({}); + } + var definition = runtime.nodes.getCredentialDefinition(opts.type); + + var sendCredentials = {}; + for (var cred in definition) { + if (definition.hasOwnProperty(cred)) { + if (definition[cred].type == "password") { + var key = 'has_' + cred; + sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; + continue; + } + sendCredentials[cred] = credentials[cred] || ''; + } + } + resolve(sendCredentials); + }) + } + +} diff --git a/red/runtime-api/index.js b/red/runtime-api/index.js new file mode 100644 index 000000000..6a75d8583 --- /dev/null +++ b/red/runtime-api/index.js @@ -0,0 +1,64 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + + /** + * A user accessing the API + * @typedef User + * @type {object} + */ + + +/** + * @namespace RED + */ +var api = module.exports = { + init: function(runtime) { + api.flows.init(runtime); + api.nodes.init(runtime); + api.settings.init(runtime); + }, + + /** + * Auth module + */ + auth: require("./auth"), + + /** + * Comms module + */ + comms: require("./comms"), + + /** + * Flows module + */ + flows: require("./flows"), + + /** + * Library module + */ + library: require("./library"), + + /** + * Nodes module + */ + nodes: require("./nodes"), + + /** + * Settings module + */ + settings: require("./settings") +} diff --git a/red/runtime-api/library.js b/red/runtime-api/library.js new file mode 100644 index 000000000..fbd265f88 --- /dev/null +++ b/red/runtime-api/library.js @@ -0,0 +1,35 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/library + */ + +module.exports = { + + /** + * Does something + */ + setEnty: function() {}, + /** + * Does something + */ + getEntry: function() {}, + /** + * Does something + */ + getEntries: function() {} +} diff --git a/red/runtime-api/nodes.js b/red/runtime-api/nodes.js new file mode 100644 index 000000000..5eb48b285 --- /dev/null +++ b/red/runtime-api/nodes.js @@ -0,0 +1,377 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +"use strict" + /** + * @namespace RED.nodes + */ + +var runtime; + +function putNode(node, enabled) { + var promise; + if (!node.err && node.enabled === enabled) { + promise = Promise.resolve(node); + } else { + if (enabled) { + promise = runtime.nodes.enableNode(node.id); + } else { + promise = runtime.nodes.disableNode(node.id); + } + } + return promise; +} + + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + + + /** + * Gets the info of an individual node set + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node set to return + * @return {Promise} - the node information + * @memberof RED.nodes + */ + getNodeInfo: function(opts) { + return new Promise(function(resolve,reject) { + var id = opts.id; + var result = redNodes.getNodeInfo(id); + if (result) { + runtime.log.audit({event: "nodes.info.get",id:id}); + delete result.loaded; + return resolve(result); + } else { + runtime.log.audit({event: "nodes.info.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }) + }, + + /** + * Gets the list of node modules installed in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the list of node modules + * @memberof RED.nodes + */ + getNodeList: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.list.get"}); + return resolve(runtime.nodes.getNodeList()); + }) + }, + + /** + * Gets an individual node's html content + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node set to return + * @param {String} opts.lang - the locale language to return + * @return {Promise} - the node html content + * @memberof RED.nodes + */ + getNodeConfig: function(opts) { + return new Promise(function(resolve,reject) { + var id = opts.id; + var lang = opts.lang; + var result = runtime.nodes.getNodeConfig(id,lang); + if (result) { + runtime.log.audit({event: "nodes.config.get",id:id}); + return resolve(result); + } else { + runtime.log.audit({event: "nodes.config.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }); + }, + /** + * Gets all node html content + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.lang - the locale language to return + * @return {Promise} - the node html content + * @memberof RED.nodes + */ + getNodeConfigs: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.configs.get"}); + return resolve(runtime.nodes.getNodeConfigs(opts.lang)); + }); + }, + + /** + * Gets the info of a node module + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to return + * @return {Promise} - the node module info + * @memberof RED.nodes + */ + getModuleInfo: function(opts) { + return new Promise(function(resolve,reject) { + var module = opts.module; + var result = redNodes.getModuleInfo(module); + if (result) { + runtime.log.audit({event: "nodes.module.get",id:id}); + return resolve(result); + } else { + runtime.log.audit({event: "nodes.module.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }) + }, + + /** + * Install a new module into the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to install + * @param {String} opts.version - (optional) the version of the module to install + * @return {Promise} - the node module info + * @memberof RED.nodes + */ + addModule: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.install",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + if (opts.module) { + var existingModule = runtime.nodes.getModuleInfo(opts.module); + if (existingModule) { + if (!opts.version || existingModule.version === opts.version) { + runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}); + let err = new Error("Module already loaded"); + err.code = "module_already_loaded"; + err.status = 400; + return reject(err); + } + if (!module.local) { + runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_not_local"}); + let err = new Error("Module not locally installed"); + err.code = "module_not_local"; + err.status = 400; + return reject(err); + } + } + runtime.nodes.installModule(opts.module,opts.version).then(function(info) { + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version}); + return resolve(info); + }).catch(function(err) { + if (err.code === 404) { + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:"not_found"}); + // TODO: code/status + err.status = 404; + } else if (err.code) { + err.status = 400; + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code}); + } else { + err.status = 400; + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()}); + } + return reject(err); + }) + } else { + runtime.log.audit({event: "nodes.install",module:opts.module,error:"invalid_request"}); + let err = new Error("Invalid request"); + err.code = "invalid_request"; + err.status = 400; + return reject(err); + } + + }); + }, + /** + * Removes a module from the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to remove + * @return {Promise} - resolves when complete + * @memberof RED.nodes + */ + removeModule: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.install",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + var module = runtime.nodes.getModuleInfo(opts.module); + if (!module) { + runtime.log.audit({event: "nodes.remove",module:opts.module,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + try { + runtime.nodes.uninstallModule(opts.module).then(function() { + runtime.log.audit({event: "nodes.remove",module:opts.module}); + resolve(); + }).catch(function(err) { + err.status = 400; + runtime.log.audit({event: "nodes.remove",module:opts.module,error:err.code||"unexpected_error",message:err.toString()}); + return reject(err); + }) + } catch(err) { + runtime.log.audit({event: "nodes.remove",module:opts.module,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Enables or disables a module in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to enable or disable + * @param {String} opts.enabled - whether the module should be enabled or disabled + * @return {Promise} - the module info object + * @memberof RED.nodes + */ + setModuleState: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.module.set",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + try { + var mod = opts.module; + var module = runtime.nodes.getModuleInfo(mod); + if (!module) { + runtime.log.audit({event: "nodes.module.set",module:mod,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + + var nodes = module.nodes; + var promises = []; + for (var i = 0; i < nodes.length; ++i) { + promises.push(putNode(nodes[i],opts.enabled)); + } + Promise.all(promises).then(function() { + return resolve(runtime.nodes.getModuleInfo(mod)); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + } catch(err) { + runtime.log.audit({event: "nodes.module.set",module:mod,enabled:opts.enabled,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Enables or disables a n individual node-set in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node-set to enable or disable + * @param {String} opts.enabled - whether the module should be enabled or disabled + * @return {Promise} - the module info object + * @memberof RED.nodes + */ + setNodeSetState: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.info.set",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + + var id = opts.id; + var enabled = opts.enabled; + try { + var node = runtime.nodes.getNodeInfo(id); + if (!node) { + runtime.log.audit({event: "nodes.info.set",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } else { + delete node.loaded; + putNode(node,enabled).then(function(result) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled}); + return resolve(result); + }).catch(function(err) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }); + } + } catch(err) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:err.code||"unexpected_error",message:err.toString()}); + res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + } + }); + }, + + /** + * TODO: getModuleCatalogs + */ + getModuleCatalogs: function() {}, + /** + * TODO: getModuleCatalog + */ + getModuleCatalog: function() {}, + + /** + * Gets the list of all icons available in the modules installed within the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the list of all icons + * @memberof RED.nodes + */ + getIconList: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.icons.get"}); + return resolve(runtime.nodes.getNodeIcons()); + }); + + }, + /** + * TODO: getIcon + */ + getIcon: function() {} +} diff --git a/red/runtime-api/settings.js b/red/runtime-api/settings.js new file mode 100644 index 000000000..e10a6d9c2 --- /dev/null +++ b/red/runtime-api/settings.js @@ -0,0 +1,257 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @namespace RED.settings + */ + +var util = require("util"); +var runtime; + +function extend(target, source) { + var keys = Object.keys(source); + var i = keys.length; + while(i--) { + var value = source[keys[i]] + var type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) { + target[keys[i]] = value; + } else if (value === null) { + if (target.hasOwnProperty(keys[i])) { + delete target[keys[i]]; + } + } else { + // Object + if (target.hasOwnProperty(keys[i])) { + target[keys[i]] = extend(target[keys[i]],value); + } else { + target[keys[i]] = value; + } + } + } + return target; +} + +function getUsername(userObj) { + var username = '__default'; + if ( userObj && userObj.name ) { + username = userObj.name; + } + return username; +} +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + /** + * Gets the runtime settings object + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the runtime settings + * @memberof RED.settings + */ + getRuntimeSettings: function(opts) { + return new Promise(function(resolve,reject) { + try { + var safeSettings = { + httpNodeRoot: runtime.settings.httpNodeRoot||"/", + version: runtime.settings.version, + user: opts.user + } + + if (util.isArray(runtime.settings.paletteCategories)) { + safeSettings.paletteCategories = runtime.settings.paletteCategories; + } + + if (runtime.settings.flowFilePretty) { + safeSettings.flowFilePretty = runtime.settings.flowFilePretty; + } + + if (!runtime.nodes.paletteEditorEnabled()) { + safeSettings.editorTheme = safeSettings.editorTheme || {}; + safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; + safeSettings.editorTheme.palette.editable = false; + } + if (runtime.storage.projects) { + var activeProject = runtime.storage.projects.getActiveProject(); + if (activeProject) { + safeSettings.project = activeProject; + } else if (runtime.storage.projects.flowFileExists()) { + safeSettings.files = { + flow: runtime.storage.projects.getFlowFilename(), + credentials: runtime.storage.projects.getCredentialsFilename() + } + } + safeSettings.git = { + globalUser: runtime.storage.projects.getGlobalGitUser() + } + } + + safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); + + runtime.settings.exportNodeSettings(safeSettings); + + resolve(safeSettings); + }catch(err) { + console.log(err); + } + }); + }, + + /** + * Gets an individual user's settings object + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the user settings + * @memberof RED.settings + */ + getUserSettings: function(opts) { + var username; + if (!opts.user || opts.user.anonymous) { + username = '_'; + } else { + username = opts.user.username; + } + return Promise.resolve(runtime.settings.getUserSettings(username)||{}); + }, + + /** + * Updates an individual user's settings object. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.settings - the updates to the user settings + * @return {Promise} - the user settings + * @memberof RED.settings + */ + updateUserSettings: function(opts) { + var username; + if (!opts.user || opts.user.anonymous) { + username = '_'; + } else { + username = opts.user.username; + } + return new Promise(function(resolve,reject) { + var currentSettings = runtime.settings.getUserSettings(username)||{}; + currentSettings = extend(currentSettings, opts.settings); + try { + runtime.settings.setUserSettings(username, currentSettings).then(function() { + runtime.log.audit({event: "settings.update",username:username}); + return resolve(); + }).catch(function(err) { + runtime.log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }); + } catch(err) { + log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")})); + log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Gets a list of a user's ssh keys + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the user's ssh keys + * @memberof RED.settings + */ + getUserKeys: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + runtime.storage.projects.ssh.listSSHKeys(username).then(function(list) { + return resolve(list); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Gets a user's ssh public key + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.id - the id of the key to return + * @return {Promise} - the user's ssh public key + * @memberof RED.settings + */ + getUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + // console.log('username:', username); + runtime.storage.projects.ssh.getSSHKey(username, opts.id).then(function(data) { + if (data) { + return resolve(data); + } else { + var err = new Error("Key not found"); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Generates a new ssh key pair + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.name - the id of the key to return + * @param {User} opts.password - (optional) the password for the key pair + * @param {User} opts.comment - (option) a comment to associate with the key pair + * @param {User} opts.size - (optional) the size of the key. Default: 2048 + * @return {Promise} - the id of the generated key + * @memberof RED.settings + */ + generateUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + runtime.storage.projects.ssh.generateSSHKey(username, opts).then(function(name) { + return resolve(name); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Deletes a user's ssh key pair + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.id - the id of the key to delete + * @return {Promise} - resolves when deleted + * @memberof RED.settings + */ + removeUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(req.user); + runtime.storage.projects.ssh.deleteSSHKey(username, opts.id).then(function() { + return resolve(); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + + } +} diff --git a/red/runtime/storage/localfilesystem/projects/ssh/index.js b/red/runtime/storage/localfilesystem/projects/ssh/index.js index dd65379cb..cc9bd8754 100644 --- a/red/runtime/storage/localfilesystem/projects/ssh/index.js +++ b/red/runtime/storage/localfilesystem/projects/ssh/index.js @@ -107,6 +107,11 @@ function getSSHKey(username, name) { function generateSSHKey(username, options) { options = options || {}; var name = options.name || ""; + if (!/^[a-zA-Z0-9\-_]+$/.test(options.name)) { + var err = new Error("Invalid SSH Key name"); + e.code = "invalid_key_name"; + return Promise.reject(err); + } return checkExistSSHKeyFiles(username, name) .then(function(result) { if ( result ) {