Merge b5fae8bb733d43faa7804168c950918cfddb1827 into bb01f26f068b8e083e35fdf19dc9b36f8cf67068

This commit is contained in:
Stephen McLaughlin 2024-12-12 15:24:56 +05:30 committed by GitHub
commit f0d20f40ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 204 additions and 103 deletions

View File

@ -231,6 +231,12 @@ button.red-ui-tray-resize-button {
border: 1px solid var(--red-ui-secondary-border-color); border: 1px solid var(--red-ui-secondary-border-color);
max-width: 450px; max-width: 450px;
} }
.form-row > div.form-tips {
width: 70%;
display: inline-block;
box-sizing: border-box;
max-width: 350px; // 100 less for label width
}
.form-tips code { .form-tips code {
border: none; border: none;
padding: auto; padding: auto;

View File

@ -23,6 +23,12 @@
</select> </select>
<span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width:65px"> <span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width:65px">
</div> </div>
<div class="form-row inbound-disabled-tip">
<label> &nbsp; </label>
<div class="form-tips" data-i18n="tcpin.tip.inbound-disabled"></div>
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left:110px;"> <div class="form-row hidden" id="node-input-host-row" style="padding-left:110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;"> <span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;">
</div> </div>
@ -70,7 +76,9 @@
color: "Silver", color: "Silver",
defaults: { defaults: {
name: {value:""}, name: {value:""},
server: {value:"server", required:true}, server: {
value: RED.settings.tcpInAllowInboundConnections === false ? "client" : "server",
},
host: { host: {
value:"", value:"",
validate:function(v, opt) { validate:function(v, opt) {
@ -107,8 +115,14 @@
var sockettype = $("#node-input-server").val(); var sockettype = $("#node-input-server").val();
if (sockettype == "client") { if (sockettype == "client") {
$("#node-input-host-row").show(); $("#node-input-host-row").show();
$(".form-row.inbound-disabled-tip").addClass("hide")
} else { } else {
$("#node-input-host-row").hide(); $("#node-input-host-row").hide();
if(RED.settings.tcpInAllowInboundConnections === false) {
$(".form-row.inbound-disabled-tip").removeClass("hide")
} else {
$(".form-row.inbound-disabled-tip").addClass("hide")
}
} }
var datamode = $("#node-input-datamode").val(); var datamode = $("#node-input-datamode").val();
var datatype = $("#node-input-datatype").val(); var datatype = $("#node-input-datatype").val();
@ -142,6 +156,11 @@
$("#node-input-usetls").on("click",function() { $("#node-input-usetls").on("click",function() {
updateTLSOptions(); updateTLSOptions();
}); });
setTimeout(function() {
// form tips have a max-width to prevent the initial edit form size calc making it overly large.
// once the form is built and displayed, remove the size limit so that it sizes with the other elements.
$('.form-tips').css({"max-width": "unset"})
}, 500)
}, },
oneditsave: function() { oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) { if (!$("#node-input-usetls").is(':checked')) {
@ -162,7 +181,10 @@
</select> </select>
<span id="node-input-port-row"><span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width: 65px"></span> <span id="node-input-port-row"><span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width: 65px"></span>
</div> </div>
<div class="form-row inbound-disabled-tip">
<label> &nbsp; </label>
<div class="form-tips" data-i18n="tcpin.tip.inbound-disabled"></div>
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;"> <div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" style="width: 60%;"> <span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" style="width: 60%;">
</div> </div>
@ -242,16 +264,23 @@
$("#node-input-host-row").hide(); $("#node-input-host-row").hide();
$("#node-input-end-row").hide(); $("#node-input-end-row").hide();
$("#node-input-tls-enable").hide(); $("#node-input-tls-enable").hide();
$(".form-row.inbound-disabled-tip").addClass("hide")
} else if (sockettype == "client"){ } else if (sockettype == "client"){
$("#node-input-port-row").show(); $("#node-input-port-row").show();
$("#node-input-host-row").show(); $("#node-input-host-row").show();
$("#node-input-end-row").show(); $("#node-input-end-row").show();
$("#node-input-tls-enable").show(); $("#node-input-tls-enable").show();
$(".form-row.inbound-disabled-tip").addClass("hide")
} else { } else {
$("#node-input-port-row").show(); $("#node-input-port-row").show();
$("#node-input-host-row").hide(); $("#node-input-host-row").hide();
$("#node-input-end-row").show(); $("#node-input-end-row").show();
$("#node-input-tls-enable").show(); $("#node-input-tls-enable").show();
if(RED.settings.tcpInAllowInboundConnections === false) {
$(".form-row.inbound-disabled-tip").removeClass("hide")
} else {
$(".form-row.inbound-disabled-tip").addClass("hide")
}
} }
}; };
updateOptions(); updateOptions();
@ -272,6 +301,11 @@
$("#node-input-usetls").on("click",function() { $("#node-input-usetls").on("click",function() {
updateTLSOptions(); updateTLSOptions();
}); });
setTimeout(function() {
// form tips have a max-width to prevent the initial edit form size calc making it overly large.
// once the form is built and displayed, remove the size limit so that it sizes with the other elements.
$('.form-tips').css({"max-width": "unset"})
}, 500)
}, },
oneditsave: function() { oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) { if (!$("#node-input-usetls").is(':checked')) {

View File

@ -19,6 +19,7 @@ module.exports = function(RED) {
let reconnectTime = RED.settings.socketReconnectTime || 10000; let reconnectTime = RED.settings.socketReconnectTime || 10000;
let socketTimeout = RED.settings.socketTimeout || null; let socketTimeout = RED.settings.socketTimeout || null;
const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000; const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000;
const allowInbound = RED.settings.tcpInAllowInboundConnections === false ? false : true
const Denque = require('denque'); const Denque = require('denque');
const net = require('net'); const net = require('net');
const tls = require('tls'); const tls = require('tls');
@ -196,8 +197,7 @@ module.exports = function(RED) {
clearTimeout(reconnectTimeout); clearTimeout(reconnectTimeout);
if (!node.connected) { done(); } if (!node.connected) { done(); }
}); });
} } else if (allowInbound) {
else {
let srv = net; let srv = net;
let connOpts; let connOpts;
if (n.tls) { if (n.tls) {
@ -308,9 +308,19 @@ module.exports = function(RED) {
}); });
} }
}); });
} else {
node.warn(RED._("tcpin.status.inbound-disabled",{host:node.host,port:node.port}));
node.status({fill:"gray",shape:"circle",text:"tcpin.status.inbound-disabled"});
} }
} }
RED.nodes.registerType("tcp in",TcpIn); RED.nodes.registerType("tcp in",TcpIn, {
settings: {
tcpInAllowInboundConnections: {
value: true,
exportable: true
}
}
});
function TcpOut(n) { function TcpOut(n) {
@ -444,7 +454,7 @@ module.exports = function(RED) {
nodeDone(); nodeDone();
}); });
} }
else { else if (allowInbound) {
const connectedSockets = new Set(); const connectedSockets = new Set();
node.status({text:RED._("tcpin.status.connections",{count:0})}); node.status({text:RED._("tcpin.status.connections",{count:0})});
let srv = net; let srv = net;
@ -517,11 +527,13 @@ module.exports = function(RED) {
}); });
} }
}); });
} else {
node.warn(RED._("tcpin.status.inbound-disabled",{host:node.host,port:node.port}));
node.status({fill:"gray",shape:"circle",text:"tcpin.status.inbound-disabled"});
} }
} }
RED.nodes.registerType("tcp out",TcpOut); RED.nodes.registerType("tcp out",TcpOut);
function TcpGet(n) { function TcpGet(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.server = n.server; this.server = n.server;

View File

@ -18,11 +18,15 @@
<script type="text/html" data-template-name="udp in"> <script type="text/html" data-template-name="udp in">
<div class="form-row"> <div class="form-row">
<label for="node-input-port"><i class="fa fa-sign-in"></i> <span data-i18n="udp.label.listen"></span></label> <label for="node-input-port"><i class="fa fa-sign-in"></i> <span data-i18n="udp.label.listen"></span></label>
<select id="node-input-multicast" style='width:70%'> <select id="node-input-multicast" style="width:70%">
<option value="false" data-i18n="udp.udpmsgs"></option> <option value="false" data-i18n="udp.udpmsgs"></option>
<option value="true" data-i18n="udp.mcmsgs"></option> <option value="true" data-i18n="udp.mcmsgs"></option>
</select> </select>
</div> </div>
<div class="form-row inbound-disabled-tip">
<label> &nbsp; </label>
<div class="form-tips" data-i18n="udp.tip.inbound-disabled"></div>
</div>
<div class="form-row node-input-group"> <div class="form-row node-input-group">
<label for="node-input-group"><i class="fa fa-list"></i> <span data-i18n="udp.label.group"></span></label> <label for="node-input-group"><i class="fa fa-list"></i> <span data-i18n="udp.label.group"></span></label>
<input type="text" id="node-input-group" placeholder="225.0.18.83"> <input type="text" id="node-input-group" placeholder="225.0.18.83">
@ -95,6 +99,11 @@
return this.name?"node_label_italic":""; return this.name?"node_label_italic":"";
}, },
oneditprepare: function() { oneditprepare: function() {
if(RED.settings.udpInAllowInboundConnections === false) {
$(".form-row.inbound-disabled-tip").removeClass("hide")
} else {
$(".form-row.inbound-disabled-tip").addClass("hide")
}
$("#node-input-multicast").on("change", function() { $("#node-input-multicast").on("change", function() {
var id = $("#node-input-multicast").val(); var id = $("#node-input-multicast").val();
if (id == "false") { if (id == "false") {
@ -121,6 +130,11 @@
RED.notify(alreadyused+" "+$("#node-input-port").val(),"warn"); RED.notify(alreadyused+" "+$("#node-input-port").val(),"warn");
} }
}); });
setTimeout(function() {
// form tips have a max-width to prevent the initial edit form size calc making it overly large.
// once the form is built and displayed, remove the size limit so that it sizes with the other elements.
$('.form-tips').css({"max-width": "unset"})
}, 500)
} }
}); });
</script> </script>

View File

@ -16,9 +16,10 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
var os = require('os'); const os = require('os');
var dgram = require('dgram'); const dgram = require('dgram');
var udpInputPortsInUse = {}; const udpInputPortsInUse = {};
const allowInbound = RED.settings.udpInAllowInboundConnections === false ? false : true
// The Input Node // The Input Node
function UDPin(n) { function UDPin(n) {
@ -29,8 +30,9 @@ module.exports = function(RED) {
this.iface = n.iface || null; this.iface = n.iface || null;
this.multicast = n.multicast; this.multicast = n.multicast;
this.ipv = n.ipv || "udp4"; this.ipv = n.ipv || "udp4";
var node = this; const node = this;
let server;
if (allowInbound) {
if (node.iface && node.iface.indexOf(".") === -1) { if (node.iface && node.iface.indexOf(".") === -1) {
try { try {
if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) { if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) {
@ -54,9 +56,8 @@ module.exports = function(RED) {
} }
} }
var opts = {type:node.ipv, reuseAddr:true}; let opts = {type:node.ipv, reuseAddr:true};
if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; } if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; }
var server;
if (!udpInputPortsInUse.hasOwnProperty(node.port)) { if (!udpInputPortsInUse.hasOwnProperty(node.port)) {
server = dgram.createSocket(opts); // default to udp4 server = dgram.createSocket(opts); // default to udp4
@ -98,7 +99,7 @@ module.exports = function(RED) {
}); });
server.on('message', function (message, remote) { server.on('message', function (message, remote) {
var msg; let msg;
if (node.datatype =="base64") { if (node.datatype =="base64") {
msg = { payload:message.toString('base64'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port }; msg = { payload:message.toString('base64'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
} else if (node.datatype =="utf8") { } else if (node.datatype =="utf8") {
@ -110,16 +111,21 @@ module.exports = function(RED) {
}); });
server.on('listening', function () { server.on('listening', function () {
var address = server.address(); const address = server.address();
node.log(RED._("udp.status.listener-at",{host:node.iface||address.address,port:address.port})); node.log(RED._("udp.status.listener-at",{host:node.iface||address.address,port:address.port}));
}); });
} else {
node.warn(RED._("udp.status.inbound-disabled",{host:node.host,port:node.port}));
node.status({fill:"gray",shape:"circle",text:"udp.status.inbound-disabled"});
}
node.on("close", function() { node.on("close", function() {
try { try {
if (server) {
if (node.multicast == "true") { server.dropMembership(node.group); } if (node.multicast == "true") { server.dropMembership(node.group); }
server.close(); server.close();
node.log(RED._("udp.status.listener-stopped")); node.log(RED._("udp.status.listener-stopped"));
}
} catch (err) { } catch (err) {
//node.error(err); //node.error(err);
} }
@ -133,7 +139,14 @@ module.exports = function(RED) {
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) { RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) {
res.json(Object.keys(udpInputPortsInUse)); res.json(Object.keys(udpInputPortsInUse));
}); });
RED.nodes.registerType("udp in",UDPin); RED.nodes.registerType("udp in",UDPin, {
settings: {
udpInAllowInboundConnections: {
value: true,
exportable: true
}
}
});

View File

@ -650,6 +650,9 @@
"never": "never - keep connection open", "never": "never - keep connection open",
"immed": "immediately - don't wait for reply" "immed": "immediately - don't wait for reply"
}, },
"tip": {
"inbound-disabled": "inbound connections are disabled for this Node-RED instance"
},
"status": { "status": {
"connecting": "connecting to __host__:__port__", "connecting": "connecting to __host__:__port__",
"connected": "connected to __host__:__port__", "connected": "connected to __host__:__port__",
@ -658,7 +661,8 @@
"connection-from": "connection from __host__:__port__", "connection-from": "connection from __host__:__port__",
"connection-closed": "connection closed from __host__:__port__", "connection-closed": "connection closed from __host__:__port__",
"connections": "__count__ connection", "connections": "__count__ connection",
"connections_plural": "__count__ connections" "connections_plural": "__count__ connections",
"inbound-disabled": "inbound connections are disabled"
}, },
"errors": { "errors": {
"connection-lost": "connection lost to __host__:__port__", "connection-lost": "connection lost to __host__:__port__",
@ -711,7 +715,8 @@
"tip": { "tip": {
"in": "Tip: Make sure your firewall will allow the data in.", "in": "Tip: Make sure your firewall will allow the data in.",
"out": "Tip: leave address and port blank if you want to set using <code>msg.ip</code> and <code>msg.port</code>.", "out": "Tip: leave address and port blank if you want to set using <code>msg.ip</code> and <code>msg.port</code>.",
"port": "Ports already in use: " "port": "Ports already in use: ",
"inbound-disabled": "inbound connections are disabled for this Node-RED instance"
}, },
"status": { "status": {
"listener-at": "udp listener at __host__:__port__", "listener-at": "udp listener at __host__:__port__",
@ -722,7 +727,8 @@
"bc-ready": "udp broadcast ready: __outport__ -> __host__:__port__", "bc-ready": "udp broadcast ready: __outport__ -> __host__:__port__",
"ready": "udp ready: __outport__ -> __host__:__port__", "ready": "udp ready: __outport__ -> __host__:__port__",
"ready-nolocal": "udp ready: __host__:__port__", "ready-nolocal": "udp ready: __host__:__port__",
"re-use": "udp re-use socket: __outport__ -> __host__:__port__" "re-use": "udp re-use socket: __outport__ -> __host__:__port__",
"inbound-disabled": "inbound connections are disabled"
}, },
"errors": { "errors": {
"access-error": "UDP access error, you may need root access for ports below 1024", "access-error": "UDP access error, you may need root access for ports below 1024",

View File

@ -19,6 +19,8 @@
or accept incoming connections.</p> or accept incoming connections.</p>
<p><b>Note: </b>On some systems you may need root or administrator access <p><b>Note: </b>On some systems you may need root or administrator access
to access ports below 1024.</p> to access ports below 1024.</p>
<p><b>Note: </b>If your node shows "inbound connections are disabled", then
it has been disabled in the Node-RED settings file.</p>
</script> </script>
<script type="text/html" data-help-name="tcp out"> <script type="text/html" data-help-name="tcp out">

View File

@ -19,8 +19,10 @@
Buffer, string, or base64 encoded string. Supports multicast.</p> Buffer, string, or base64 encoded string. Supports multicast.</p>
<p>It also provides <code>msg.ip</code> and <code>msg.port</code> set to the <p>It also provides <code>msg.ip</code> and <code>msg.port</code> set to the
ip address and port from which the message was received.</p> ip address and port from which the message was received.</p>
<p><b>Note</b>: On some systems you may need root or administrator access to use <p><b>Note: </b>On some systems you may need root or administrator access
ports below 1024 and/or broadcast.</p> to access ports below 1024.</p>
<p><b>Note: </b>If your node shows "inbound connections are disabled", then
it has been disabled in the Node-RED settings file.</p>
</script> </script>
<script type="text/html" data-help-name="udp out"> <script type="text/html" data-help-name="udp out">

View File

@ -486,6 +486,8 @@ module.exports = {
* - socketReconnectTime * - socketReconnectTime
* - socketTimeout * - socketTimeout
* - tcpMsgQueueSize * - tcpMsgQueueSize
* - tcpInAllowInboundConnections
* - udpInAllowInboundConnections
* - inboundWebSocketTimeout * - inboundWebSocketTimeout
* - tlsConfigDisableLocalFiles * - tlsConfigDisableLocalFiles
* - webSocketNodeVerifyClient * - webSocketNodeVerifyClient
@ -560,6 +562,16 @@ module.exports = {
*/ */
//tcpMsgQueueSize: 2000, //tcpMsgQueueSize: 2000,
/** Permit TCP nodes to expose inbound connections. Set value to `false` to disable the inbound modes.
* defaults to true
*/
//tcpInAllowInboundConnections: true,
/** Permit UDP nodes to expose inbound connections. Set value to `false` to disable the inbound modes.
* defaults to true
*/
//udpInAllowInboundConnections: true,
/** Timeout in milliseconds for inbound WebSocket connections that do not /** Timeout in milliseconds for inbound WebSocket connections that do not
* match any configured node. Defaults to 5000 * match any configured node. Defaults to 5000
*/ */

View File

@ -157,7 +157,7 @@ describe('TCP in Node', function() {
testTCP0(flow, ["foo\nbar\nbaz"], ["foo\nbar\nbaz"], done); testTCP0(flow, ["foo\nbar\nbaz"], ["foo\nbar\nbaz"], done);
}); });
it('should recv data (Stream/Base64)', function(done) { it('should recv data (Single/Base64)', function(done) {
var flow = [{id:"n1", type:"tcp in", server:"server", host:"localhost", port:port, datamode:"single", datatype:"base64", newline:"", topic:"", base64:false, wires:[["n2"]] }, var flow = [{id:"n1", type:"tcp in", server:"server", host:"localhost", port:port, datamode:"single", datatype:"base64", newline:"", topic:"", base64:false, wires:[["n2"]] },
{id:"n2", type:"helper"}]; {id:"n2", type:"helper"}];
testTCP0(flow, ["foo"], [Buffer("foo").toString('base64')], done); testTCP0(flow, ["foo"], [Buffer("foo").toString('base64')], done);
@ -227,7 +227,7 @@ describe('TCP in Node', function() {
testTCP1(flow, ["foo\nbar\nbaz"], ["foo\nbar\nbaz"], done); testTCP1(flow, ["foo\nbar\nbaz"], ["foo\nbar\nbaz"], done);
}); });
it('should connect & recv data (Stream/Base64)', function(done) { it('should connect & recv data (Single/Base64)', function(done) {
var flow = [{id:"n1", type:"tcp in", server:"client", host:"localhost", port:server_port, datamode:"single", datatype:"base64", newline:"", topic:"", base64:false, wires:[["n2"]] }, var flow = [{id:"n1", type:"tcp in", server:"client", host:"localhost", port:server_port, datamode:"single", datatype:"base64", newline:"", topic:"", base64:false, wires:[["n2"]] },
{id:"n2", type:"helper"}]; {id:"n2", type:"helper"}];
testTCP1(flow, ["foo"], [Buffer("foo").toString('base64')], done); testTCP1(flow, ["foo"], [Buffer("foo").toString('base64')], done);