diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b4712aa6..1ff6df9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +#### 0.15.3: Maintenance Release + + - Tcpgetfix: Another small check (#1070) + - TCPGet: Ensure done() is called only once (#1068) + - Allow $ and _ at start of property identifiers Fixes #1063 + - TCPGet: Separated the node.connected property for each instance (#1062) + - Corrected 'overide' typo in XML node help (#1061) + - TCPGet: Last property check (hopefully) (#1059) + - Add additional safety checks to avoid acting on non-existent objects (#1057) + - add --title for process name to command line options + - add indicator for fire once on inject node + - reimplement $(env var) replace to share common code. + - Fix error message for missing node html file, and add test. + - Let credentials also use $(...) substitutions from ENV + - Rename insecureRedirect to requireHttps + - Add setting to cause insecure redirect (#1054) + - Palette editor fixes (#1033) + - Close comms on stopServer in test helper (#1020) + - Tcpgetfix (#1050) + - TCPget: Store incoming messages alongside the client object to keep reference + - Merge remote-tracking branch 'upstream/master' into tcpgetfix + - TCPget can now handle concurrent sessions (#1042) + - Better scope handling + - Add security checks + - small change to udp httpadmin + - Fix comparison to "" in tcpin + - Change scope of clients object + - Works when connection is left open + - First release of multi connection tcpget + - Fix node.error() not printing when passed false (#1037) + - fix test for CSV array input + - different test for Pi (rather than use serial port name) + - Fix missing 0 handling for css node with array input + + #### 0.15.2: Maintenance Release - Revert bidi changes to nodes and hide menu option until fixed Fixes #1024 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb7b1628c..2ee11a240 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,21 +38,13 @@ If you want to raise a pull-request with a new feature, or a refactoring of existing code, it may well get rejected if you haven't discussed it on the [mailing list](https://groups.google.com/forum/#!forum/node-red) first. -### Contributor License Agreement +All contributors need to sign the JS Foundation's Contributor License Agreement. +It is an online process and quick to do. You can read the details of the agreement +here: https://cla.js.foundation/node-red/node-red. -In order for us to accept pull-requests, the contributor must first complete -a Contributor License Agreement (CLA). This clarifies the intellectual -property license granted with any contribution. It is for your protection as a -Contributor as well as the protection of IBM and its customers; it does not -change your rights to use your own Contributions for any other purpose. +If you raise a pull-request without having signed the CLA, you will be prompted +to do so automatically. -You can download the CLAs here: - - - [individual](http://nodered.org/cla/node-red-cla-individual.pdf) - - [corporate](http://nodered.org/cla/node-red-cla-corporate.pdf) - -If you are an IBMer, please contact us directly as the contribution process is -slightly different. ### Coding standards diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 06359a9ec..67fb03eed 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -14,9 +14,6 @@ * limitations under the License. **/ (function($) { - - - var allOptions = { msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression}, diff --git a/editor/js/ui/palette-editor.js b/editor/js/ui/palette-editor.js index d5a537699..2d74f35c3 100644 --- a/editor/js/ui/palette-editor.js +++ b/editor/js/ui/palette-editor.js @@ -255,7 +255,9 @@ RED.palette.editor = (function() { nodeEntry.removeButton.hide(); } else { nodeEntry.enableButton.removeClass('disabled'); - nodeEntry.removeButton.show(); + if (moduleInfo.local) { + nodeEntry.removeButton.show(); + } if (activeTypeCount === 0) { nodeEntry.enableButton.html(RED._('palette.editor.enableall')); } else { diff --git a/nodes/core/core/20-inject.html b/nodes/core/core/20-inject.html index a362ecedd..b7e0f996c 100644 --- a/nodes/core/core/20-inject.html +++ b/nodes/core/core/20-inject.html @@ -201,6 +201,11 @@ icon: "inject.png", label: function() { var suffix = ""; + // if fire once then add small indication + if (this.once) { + suffix = " ¹"; + } + // but replace with repeat one if set to repeat if (this.repeat || this.crontab) { suffix = " ↻"; } diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index e24ae7f1d..a92ca680d 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -23,9 +23,9 @@ module.exports = function(RED) { var gpioCommand = __dirname+'/nrgpio'; try { - fs.statSync("/dev/ttyAMA0"); // unlikely if not on a Pi + var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); + if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); } } catch(err) { - //RED.log.info(RED._("rpi-gpio.errors.ignorenode")); throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); } diff --git a/nodes/core/io/31-tcpin.js b/nodes/core/io/31-tcpin.js index 1e8ae8795..14464b02e 100644 --- a/nodes/core/io/31-tcpin.js +++ b/nodes/core/io/31-tcpin.js @@ -83,7 +83,7 @@ module.exports = function(RED) { } }); client.on('end', function() { - if (!node.stream || (node.datatype == "utf8" && node.newline != "" && buffer.length > 0)) { + if (!node.stream || (node.datatype == "utf8" && node.newline !== "" && buffer.length > 0)) { var msg = {topic:node.topic, payload:buffer}; msg._session = {type:"tcp",id:id}; if (buffer.length !== 0) { @@ -407,69 +407,88 @@ module.exports = function(RED) { } // jshint ignore:line } - var buf; - if (this.out == "count") { - if (this.splitc === 0) { buf = new Buffer(1); } - else { buf = new Buffer(this.splitc); } - } - else { buf = new Buffer(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully - - this.connected = false; var node = this; - var client; - var m; + + var clients = {}; this.on("input", function(msg) { - m = msg; var i = 0; if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) { msg.payload = msg.payload.toString(); } - if (!node.connected) { - client = net.Socket(); - if (socketTimeout !== null) { client.setTimeout(socketTimeout); } - var host = node.server || msg.host; - var port = node.port || msg.port; + + var host = node.server || msg.host; + var port = node.port || msg.port; + + // Store client information independently + // the clients object will have: + // clients[id].client, clients[id].msg, clients[id].timeout + var connection_id = host + ":" + port; + clients[connection_id] = clients[connection_id] || {}; + clients[connection_id].msg = msg; + clients[connection_id].connected = clients[connection_id].connected || false; + + if (!clients[connection_id].connected) { + var buf; + if (this.out == "count") { + if (this.splitc === 0) { buf = new Buffer(1); } + else { buf = new Buffer(this.splitc); } + } + else { buf = new Buffer(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully + + clients[connection_id].client = net.Socket(); + if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);} if (host && port) { - client.connect(port, host, function() { + clients[connection_id].client.connect(port, host, function() { //node.log(RED._("tcpin.errors.client-connected")); node.status({fill:"green",shape:"dot",text:"common.status.connected"}); - node.connected = true; - client.write(msg.payload); + if (clients[connection_id] && clients[connection_id].client) { + clients[connection_id].connected = true; + clients[connection_id].client.write(clients[connection_id].msg.payload); + } }); } else { node.warn(RED._("tcpin.errors.no-host")); } - client.on('data', function(data) { + clients[connection_id].client.on('data', function(data) { if (node.out == "sit") { // if we are staying connected just send the buffer - m.payload = data; - node.send(m); + if (clients[connection_id]) { + clients[connection_id].msg.payload = data; + node.send(clients[connection_id].msg); + } } else if (node.splitc === 0) { - msg.payload = data; - node.send(msg); + clients[connection_id].msg.payload = data; + node.send(clients[connection_id].msg); } else { for (var j = 0; j < data.length; j++ ) { if (node.out === "time") { - // do the timer thing - if (node.tout) { - i += 1; - buf[i] = data[j]; - } - else { - node.tout = setTimeout(function () { - node.tout = null; - msg.payload = new Buffer(i+1); - buf.copy(msg.payload,0,0,i+1); - node.send(msg); - if (client) { node.status({}); client.destroy(); } - }, node.splitc); - i = 0; - buf[0] = data[j]; + if (clients[connection_id]) { + // do the timer thing + if (clients[connection_id].timeout) { + i += 1; + buf[i] = data[j]; + } + else { + clients[connection_id].timeout = setTimeout(function () { + if (clients[connection_id]) { + clients[connection_id].timeout = null; + clients[connection_id].msg.payload = new Buffer(i+1); + buf.copy(clients[connection_id].msg.payload,0,0,i+1); + node.send(clients[connection_id].msg); + if (clients[connection_id].client) { + node.status({}); clients[connection_id].client.destroy(); + delete clients[connection_id]; + } + } + }, node.splitc); + i = 0; + buf[0] = data[j]; + } } } // count bytes into a buffer... @@ -477,11 +496,16 @@ module.exports = function(RED) { buf[i] = data[j]; i += 1; if ( i >= node.splitc) { - msg.payload = new Buffer(i); - buf.copy(msg.payload,0,0,i); - node.send(msg); - if (client) { node.status({}); client.destroy(); } - i = 0; + if (clients[connection_id]) { + clients[connection_id].msg.payload = new Buffer(i); + buf.copy(clients[connection_id].msg.payload,0,0,i); + node.send(clients[connection_id].msg); + if (clients[connection_id].client) { + node.status({}); clients[connection_id].client.destroy(); + delete clients[connection_id]; + } + i = 0; + } } } // look for a char @@ -489,61 +513,101 @@ module.exports = function(RED) { buf[i] = data[j]; i += 1; if (data[j] == node.splitc) { - msg.payload = new Buffer(i); - buf.copy(msg.payload,0,0,i); - node.send(msg); - if (client) { node.status({}); client.destroy(); } - i = 0; + if (clients[connection_id]) { + clients[connection_id].msg.payload = new Buffer(i); + buf.copy(clients[connection_id].msg.payload,0,0,i); + node.send(clients[connection_id].msg); + if (clients[connection_id].client) { + node.status({}); clients[connection_id].client.destroy(); + delete clients[connection_id]; + } + i = 0; + } } } } } }); - client.on('end', function() { + clients[connection_id].client.on('end', function() { //console.log("END"); - node.connected = false; node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); - client = null; + if (clients[connection_id] && clients[connection_id].client) { + clients[connection_id].connected = false; + clients[connection_id].client = null; + } }); - client.on('close', function() { + clients[connection_id].client.on('close', function() { //console.log("CLOSE"); - node.connected = false; - if (node.done) { node.done(); } + if (clients[connection_id]) { + clients[connection_id].connected = false; + } + + var anyConnected = false; + + for (var client in clients) { + if (clients[client].connected) { + anyConnected = true; + break; + } + } + if (node.done && !anyConnected) { + clients = {}; + node.done(); + } }); - client.on('error', function() { + clients[connection_id].client.on('error', function() { //console.log("ERROR"); - node.connected = false; node.status({fill:"red",shape:"ring",text:"common.status.error"}); - node.error(RED._("tcpin.errors.connect-fail"),msg); - if (client) { client.destroy(); } + node.error(RED._("tcpin.errors.connect-fail") + " " + connection_id, msg); + if (clients[connection_id] && clients[connection_id].client) { + clients[connection_id].connected = false; + clients[connection_id].client.destroy(); + delete clients[connection_id]; + } }); - client.on('timeout',function() { + clients[connection_id].client.on('timeout',function() { //console.log("TIMEOUT"); - node.connected = false; + clients[connection_id].connected = false; node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"}); //node.warn(RED._("tcpin.errors.connect-timeout")); - if (client) { - client.connect(port, host, function() { - node.connected = true; + if (clients[connection_id] && clients[connection_id].client) { + clients[connection_id].client.connect(port, host, function() { + clients[connection_id].connected = true; node.status({fill:"green",shape:"dot",text:"common.status.connected"}); }); } }); } - else { client.write(msg.payload); } + else { + if (clients[connection_id] && clients[connection_id].client) { + clients[connection_id].client.write(clients[connection_id].msg.payload); + } + } }); this.on("close", function(done) { node.done = done; - if (client) { - client.destroy(); + for (var client in clients) { + clients[client].client.destroy(); } node.status({}); - if (!node.connected) { done(); } + + var anyConnected = false; + for (var c in clients) { + if (clients[c].connected) { + anyConnected = true; + break; + } + } + + if (!anyConnected) { + clients = {}; + done(); + } }); } diff --git a/nodes/core/io/32-udp.js b/nodes/core/io/32-udp.js index 736a3fbd2..66e3a2c0a 100644 --- a/nodes/core/io/32-udp.js +++ b/nodes/core/io/32-udp.js @@ -102,7 +102,7 @@ module.exports = function(RED) { try { server.bind(node.port,node.iface); } catch(e) { } // Don't worry if already bound } - RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-in.read'), function(req,res) { + RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) { res.json(Object.keys(udpInputPortsInUse)); }); RED.nodes.registerType("udp in",UDPin); diff --git a/nodes/core/parsers/70-XML.html b/nodes/core/parsers/70-XML.html index d5e32a90a..df322097f 100644 --- a/nodes/core/parsers/70-XML.html +++ b/nodes/core/parsers/70-XML.html @@ -36,7 +36,7 @@

A function that parses the msg.payload to convert xml to/from a javascript object. Places the result in the payload.

If the input is a string it tries to parse it as XML and creates a javascript object.

If the input is a javascript object it tries to build an XML string.

-

You can also pass in a msg.options object to overide all the multitude of parameters. See +

You can also pass in a msg.options object to override all the multitude of parameters. See the xml2js docs for more information.

If set, options in the edit dialogue override those passed in on the msg.options object.

diff --git a/package.json b/package.json index 5ff524ec9..91bbcb3f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name" : "node-red", - "version" : "0.15.2", + "version" : "0.15.3", "description" : "A visual tool for wiring the Internet of Things", "homepage" : "http://nodered.org", "license" : "Apache-2.0", diff --git a/red.js b/red.js index c703ae878..f521fd2dd 100755 --- a/red.js +++ b/red.js @@ -33,17 +33,20 @@ var settingsFile; var flowFile; var knownOpts = { - "settings":[path], - "userDir":[path], + "help": Boolean, "port": Number, - "v": Boolean, - "help": Boolean + "settings": [path], + "title": String, + "userDir": [path], + "verbose": Boolean }; var shortHands = { - "s":["--settings"], - "u":["--userDir"], + "?":["--help"], "p":["--port"], - "?":["--help"] + "s":["--settings"], + "t":["--help"], + "u":["--userDir"], + "v":["--verbose"] }; nopt.invalidHandler = function(k,v,t) { // TODO: console.log(k,v,t); @@ -54,14 +57,15 @@ var parsedArgs = nopt(knownOpts,shortHands,process.argv,2) if (parsedArgs.help) { console.log("Node-RED v"+RED.version()); console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR]"); - console.log(" [--port PORT] [flows.json]"); + console.log(" [--port PORT] [--title TITLE] [flows.json]"); console.log(""); console.log("Options:"); - console.log(" -s, --settings FILE use specified settings file"); - console.log(" -u, --userDir DIR use specified user directory"); console.log(" -p, --port PORT port to listen on"); - console.log(" -v enable verbose output"); - console.log(" -?, --help show usage"); + console.log(" -s, --settings FILE use specified settings file"); + console.log(" --title TITLE process window title"); + console.log(" -u, --userDir DIR use specified user directory"); + console.log(" -v, --verbose enable verbose output"); + console.log(" -?, --help show this help"); console.log(""); console.log("Documentation can be found at http://nodered.org"); process.exit(); @@ -121,9 +125,9 @@ if (parsedArgs.v) { } if (settings.https) { - server = https.createServer(settings.https,function(req,res){app(req,res);}); + server = https.createServer(settings.https,function(req,res) {app(req,res);}); } else { - server = http.createServer(function(req,res){app(req,res);}); + server = http.createServer(function(req,res) {app(req,res);}); } server.setMaxListeners(0); @@ -224,7 +228,6 @@ if (settings.httpNodeRoot !== false && settings.httpNodeAuth) { if (settings.httpNodeRoot !== false) { app.use(settings.httpNodeRoot,RED.httpNode); } - if (settings.httpStatic) { settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; if (settings.httpStaticAuth) { @@ -265,7 +268,7 @@ RED.start().then(function() { if (settings.httpAdminRoot === false) { RED.log.info(RED.log._("server.admin-ui-disabled")); } - process.title = 'node-red'; + process.title = parsedArgs.title || 'node-red'; RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); }); } else { @@ -280,7 +283,6 @@ RED.start().then(function() { } }); - process.on('uncaughtException',function(err) { util.log('[red] Uncaught Exception:'); if (err.stack) { diff --git a/red/api/index.js b/red/api/index.js index 330b3802e..e4f01fcbd 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -87,6 +87,16 @@ function init(_server,_runtime) { if (!settings.disableEditor) { ui.init(runtime); var editorApp = express(); + if (settings.requireHttps === true) { + editorApp.enable('trust proxy'); + editorApp.use(function (req, res, next) { + if (req.secure) { + next(); + } else { + res.redirect('https://' + req.headers.host + req.originalUrl); + } + }); + } editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); editorApp.get("/icons/:icon",ui.icon); theme.init(runtime); diff --git a/red/runtime/nodes/Node.js b/red/runtime/nodes/Node.js index ed0503ec5..837b49a11 100644 --- a/red/runtime/nodes/Node.js +++ b/red/runtime/nodes/Node.js @@ -236,7 +236,9 @@ Node.prototype.warn = function(msg) { }; Node.prototype.error = function(logMessage,msg) { - logMessage = logMessage || ""; + if (typeof logMessage != 'boolean') { + logMessage = logMessage || ""; + } log_helper(this, Log.ERROR, logMessage); /* istanbul ignore else */ if (msg) { diff --git a/red/runtime/nodes/flows/Flow.js b/red/runtime/nodes/flows/Flow.js index e4dedd97d..05f48d684 100644 --- a/red/runtime/nodes/flows/Flow.js +++ b/red/runtime/nodes/flows/Flow.js @@ -259,32 +259,6 @@ function Flow(global,flow) { } } } - -} - -var EnvVarPropertyRE = /^\$\((\S+)\)$/; - -function mapEnvVarProperties(obj,prop) { - if (Buffer.isBuffer(obj[prop])) { - return; - } else if (Array.isArray(obj[prop])) { - for (var i=0;i 0) { + while (changedSubflowStack.length > 0) { var subflowId = changedSubflowStack.pop(); for (id in newConfig.allNodes) { if (newConfig.allNodes.hasOwnProperty(id)) { @@ -350,7 +376,7 @@ module.exports = { // Traverse the links of all modified nodes to mark the connected nodes var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired); var visited = {}; - while(modifiedNodes.length > 0) { + while (modifiedNodes.length > 0) { node = modifiedNodes.pop(); if (!visited[node]) { visited[node] = true; diff --git a/red/runtime/nodes/index.js b/red/runtime/nodes/index.js index 609d2e345..585447a2d 100644 --- a/red/runtime/nodes/index.js +++ b/red/runtime/nodes/index.js @@ -21,6 +21,7 @@ var fs = require("fs"); var registry = require("./registry"); var credentials = require("./credentials"); var flows = require("./flows"); +var flowUtil = require("./flows/util") var context = require("./context"); var Node = require("./Node"); var log = require("../log"); @@ -69,6 +70,12 @@ function createNode(node,def) { var creds = credentials.get(id); if (creds) { //console.log("Attaching credentials to ",node.id); + // allow $(foo) syntax to substitute env variables for credentials also... + for (var p in creds) { + if (creds.hasOwnProperty(p)) { + flowUtil.mapEnvVarProperties(creds,p); + } + } node.credentials = creds; } else if (credentials.getDefinition(node.type)) { node.credentials = {}; @@ -146,7 +153,6 @@ module.exports = { // disableFlow: flows.disableFlow, // enableFlow: flows.enableFlow, - // Credentials addCredentials: credentials.add, getCredentials: credentials.get, diff --git a/red/runtime/nodes/registry/loader.js b/red/runtime/nodes/registry/loader.js index 3ad53d269..7a9f38ea5 100644 --- a/red/runtime/nodes/registry/loader.js +++ b/red/runtime/nodes/registry/loader.js @@ -87,10 +87,10 @@ function createNodeApi(node) { red.server = runtime.adminApi.server; } else { red.comms = { - publish: function(){} + publish: function() {} }; red.library = { - register: function(){} + register: function() {} }; red.auth = { needsPermission: function() {} @@ -203,7 +203,7 @@ function loadNodeConfig(fileInfo) { if (!node.types) { node.types = []; } - node.err = "Error: "+file+" does not exist"; + node.err = "Error: "+node.template+" does not exist"; } else { node.types = []; node.err = err.toString(); @@ -215,7 +215,7 @@ function loadNodeConfig(fileInfo) { var regExp = /