1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Add TLS node and update MQTT/HTTP nodes to use it

This commit is contained in:
Nick O'Leary 2016-04-27 12:31:54 +01:00
parent b744491dd2
commit 1e2521c37a
7 changed files with 257 additions and 43 deletions

73
nodes/core/io/05-tls.html Normal file
View File

@ -0,0 +1,73 @@
<!--
Copyright 2016 IBM Corp.
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.
-->
<script type="text/x-red" data-template-name="tls-config">
<div class="form-row">
<label style="width: 120px;" for="node-config-input-cert"><i class="fa fa-file-text-o"></i> <span data-i18n="tls.label.cert"></span></label>
<input style="width: 60%;" type="text" id="node-config-input-cert" data-i18n="[placeholder]tls.placeholder.cert">
</div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-key"><i class="fa fa-file-text-o"></i> <span data-i18n="tls.label.key"></span></label>
<input style="width: 60%;" type="text" id="node-config-input-key" data-i18n="[placeholder]tls.placeholder.key">
</div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-ca"><i class="fa fa-file-text-o"></i> <span data-i18n="tls.label.ca"></span></label>
<input style="width: 60%;" type="text" id="node-config-input-ca" data-i18n="[placeholder]tls.placeholder.ca">
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-verifyservercert" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-verifyservercert" style="width: 70%;" data-i18n="tls.label.verify-server-cert"></label>
</div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input style="width: 60%;" type="text" id="node-config-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="tls-config">
<p>Configuration options for TLS connections.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('tls-config',{
category: 'config',
defaults: {
name: {value:""},
cert: {value:"", validate: function(v) {
var currentKey = $("#node-config-input-key").val();
if (currentKey === undefined) {
currentKey = this.key;
}
return currentKey === '' || v != '';
}},
key: {value:"", validate: function(v) {
var currentCert = $("#node-config-input-cert").val();
if (currentCert === undefined) {
currentCert = this.cert;
}
return currentCert === '' || v != '';
}},
ca: {value:""},
verifyservercert: {value: true}
},
label: function() {
return this.name || this._("tls.tls");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

69
nodes/core/io/05-tls.js Normal file
View File

@ -0,0 +1,69 @@
/**
* Copyright 2016 IBM Corp.
*
* 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.
**/
var fs = require('fs');
module.exports = function(RED) {
"use strict";
function TLSConfig(n) {
RED.nodes.createNode(this,n);
this.valid = true;
var certPath = n.cert.trim();
var keyPath = n.key.trim();
var caPath = n.ca.trim();
if ( (certPath.length > 0) !== (keyPath.length > 0)) {
this.valid = false;
this.error(RED._("tls.error.missing-file"));
return;
}
this.verifyservercert = n.verifyservercert;
try {
if (certPath) {
this.cert = fs.readFileSync(certPath);
}
if (keyPath) {
this.key = fs.readFileSync(keyPath);
}
if (caPath) {
this.ca = fs.readFileSync(caPath);
}
} catch(err) {
this.valid = false;
this.error(err.toString());
return;
}
}
RED.nodes.registerType("tls-config",TLSConfig);
TLSConfig.prototype.addTLSOptions = function(opts) {
if (this.valid) {
if (this.key) {
opts.key = this.key;
}
if (this.cert) {
opts.cert = this.cert;
}
if (this.ca) {
opts.ca = this.ca;
}
opts.rejectUnauthorized = this.verifyservercert;
}
return opts;
}
}

View File

@ -150,11 +150,17 @@
<label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> <span data-i18n="mqtt.label.port"></span></label> <label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> <span data-i18n="mqtt.label.port"></span></label>
<input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:45px"> <input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:45px">
</div> </div>
<div class="form-row">
<input type="checkbox" id="node-config-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-usetls" style="width: auto" data-i18n="mqtt.label.use-tls"></label>
<div id="node-config-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-config-input-tls"><span data-i18n="mqtt.label.tls-config"></span></label><input style="width: 300px;" type="text" id="node-config-input-tls">
</div>
</div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-clientid"><i class="fa fa-tag"></i> <span data-i18n="mqtt.label.clientid"></span></label> <label for="node-config-input-clientid"><i class="fa fa-tag"></i> <span data-i18n="mqtt.label.clientid"></span></label>
<input type="text" id="node-config-input-clientid" data-i18n="[placeholder]mqtt.placeholder.clientid"> <input type="text" id="node-config-input-clientid" data-i18n="[placeholder]mqtt.placeholder.clientid">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-keepalive" style="width: auto"><i class="fa fa-clock-o"></i> <span data-i18n="mqtt.label.keepalive"></span></label> <label for="node-config-input-keepalive" style="width: auto"><i class="fa fa-clock-o"></i> <span data-i18n="mqtt.label.keepalive"></span></label>
<input type="text" id="node-config-input-keepalive" style="width: 50px"> <input type="text" id="node-config-input-keepalive" style="width: 50px">
@ -175,14 +181,6 @@
<label for="node-config-input-password"><i class="fa fa-lock"></i> <span data-i18n="common.label.password"></span></label> <label for="node-config-input-password"><i class="fa fa-lock"></i> <span data-i18n="common.label.password"></span></label>
<input type="password" id="node-config-input-password"> <input type="password" id="node-config-input-password">
</div> </div>
<div class="form-row">
<input type="checkbox" id="node-config-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-usetls" style="width: 70%;" data-i18n="mqtt.label.use-tls"></label>
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-verifyservercert" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-verifyservercert" style="width: 70%;" data-i18n="mqtt.label.verify-server-cert"></label>
</div>
</div> </div>
<div id="mqtt-broker-tab-birth" style="display:none"> <div id="mqtt-broker-tab-birth" style="display:none">
<div class="form-row"> <div class="form-row">
@ -240,6 +238,7 @@
defaults: { defaults: {
broker: {value:"",required:true}, broker: {value:"",required:true},
port: {value:1883,required:true,validate:RED.validators.number()}, port: {value:1883,required:true,validate:RED.validators.number()},
tls: {type:"tls-config",required: false},
clientid: { value:"", validate: function(v) { clientid: { value:"", validate: function(v) {
if ($("#node-config-input-clientid").length) { if ($("#node-config-input-clientid").length) {
// Currently editing the node // Currently editing the node
@ -303,10 +302,6 @@
this.usetls = false; this.usetls = false;
$("#node-config-input-usetls").prop("checked",false); $("#node-config-input-usetls").prop("checked",false);
} }
if (typeof this.verifyservercert === 'undefined'){
this.verifyservercert = true;
$("#node-config-input-verifyservercert").prop("checked",true);
}
if (typeof this.compatmode === 'undefined'){ if (typeof this.compatmode === 'undefined'){
this.compatmode = true; this.compatmode = true;
$("#node-config-input-compatmode").prop('checked', true); $("#node-config-input-compatmode").prop('checked', true);
@ -326,11 +321,9 @@
function updateTLSOptions() { function updateTLSOptions() {
if ($("#node-config-input-usetls").is(':checked')) { if ($("#node-config-input-usetls").is(':checked')) {
$("#node-config-input-verifyservercert").prop("disabled", false); $("#node-config-row-tls").show();
$("#node-config-input-verifyservercert").next().css("color","");
} else { } else {
$("#node-config-input-verifyservercert").prop("disabled", true); $("#node-config-row-tls").hide();
$("#node-config-input-verifyservercert").next().css("color","#aaa");
} }
} }
updateTLSOptions(); updateTLSOptions();
@ -350,6 +343,11 @@
$("#node-config-input-cleansession").on("click",function() { $("#node-config-input-cleansession").on("click",function() {
updateClientId(); updateClientId();
}); });
},
oneditsave: function() {
if (!$("#node-config-input-usetls").is(':checked')) {
$("#node-config-input-tls").val("");
}
} }
}); });
</script> </script>

View File

@ -114,8 +114,18 @@ module.exports = function(RED) {
this.options.protocolId = 'MQIsdp'; this.options.protocolId = 'MQIsdp';
this.options.protocolVersion = 3; this.options.protocolVersion = 3;
} }
if (this.usetls && n.tls) {
this.options.rejectUnauthorized = (this.verifyservercert == "true" || this.verifyservercert === true) var tlsNode = RED.nodes.getNode(n.tls);
if (tlsNode) {
tlsNode.addTLSOptions(this.options);
}
}
// If there's no rejectUnauthorized already, then this could be an
// old config where this option was provided on the broker node and
// not the tls node
if (typeof this.options.rejectUnauthorized === 'undefined') {
this.options.rejectUnauthorized = (this.verifyservercert == "true" || this.verifyservercert === true);
}
if (n.willTopic) { if (n.willTopic) {
this.options.will = { this.options.will = {
@ -284,6 +294,9 @@ module.exports = function(RED) {
done(); done();
}); });
this.client.end(); this.client.end();
} if (this.connecting) {
node.client.end();
done();
} else { } else {
done(); done();
} }

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2013, 2015 IBM Corp. Copyright 2013, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -29,19 +29,31 @@
<label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label> <label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
<input type="text" id="node-input-url" placeholder="http://"> <input type="text" id="node-input-url" placeholder="http://">
</div> </div>
<div class="form-row">
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row"> <div class="form-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-useAuth" style="display: inline-block; width: auto; vertical-align: top;"> <input type="checkbox" id="node-input-useAuth" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-useAuth" style="width: 70%;"><span data-i18n="httpin.basicauth"></span></label> <label for="node-input-useAuth" style="width: 70%;"><span data-i18n="httpin.basicauth"></span></label>
<div style="margin-left: 20px" class="node-input-useAuth-row hide">
<div class="form-row">
<label for="node-input-user"><i class="fa fa-user"></i> <span data-i18n="common.label.username"></span></label>
<input type="text" id="node-input-user">
</div>
<div class="form-row">
<label for="node-input-password"><i class="fa fa-lock"></i> <span data-i18n="common.label.password"></span></label>
<input type="password" id="node-input-password">
</div>
</div>
</div> </div>
<div class="form-row node-input-useAuth-row">
<label for="node-input-user"><i class="fa fa-user"></i> <span data-i18n="common.label.username"></span></label>
<input type="text" id="node-input-user">
</div>
<div class="form-row node-input-useAuth-row">
<label for="node-input-password"><i class="fa fa-lock"></i> <span data-i18n="common.label.password"></span></label>
<input type="password" id="node-input-password">
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-ret"><i class="fa fa-arrow-left"></i> <span data-i18n="httpin.label.return"></span></label> <label for="node-input-ret"><i class="fa fa-arrow-left"></i> <span data-i18n="httpin.label.return"></span></label>
<select type="text" id="node-input-ret" style="width:72%;"> <select type="text" id="node-input-ret" style="width:72%;">
@ -91,8 +103,7 @@
method:{value:"GET"}, method:{value:"GET"},
ret: {value:"txt"}, ret: {value:"txt"},
url:{value:""}, url:{value:""},
//user -> credentials tls: {type:"tls-config",required: false}
//pass -> credentials
}, },
credentials: { credentials: {
user: {type:"text"}, user: {type:"text"},
@ -108,14 +119,6 @@
return this.name?"node_label_italic":""; return this.name?"node_label_italic":"";
}, },
oneditprepare: function() { oneditprepare: function() {
if (this.credentials.user || this.credentials.has_password) {
$('#node-input-useAuth').prop('checked', true);
$(".node-input-useAuth-row").show();
} else {
$('#node-input-useAuth').prop('checked', false);
$(".node-input-useAuth-row").hide();
}
$("#node-input-useAuth").change(function() { $("#node-input-useAuth").change(function() {
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
$(".node-input-useAuth-row").show(); $(".node-input-useAuth-row").show();
@ -125,7 +128,29 @@
$('#node-input-password').val(''); $('#node-input-password').val('');
} }
}); });
if (this.credentials.user || this.credentials.has_password) {
$('#node-input-useAuth').prop('checked', true);
} else {
$('#node-input-useAuth').prop('checked', false);
}
$("#node-input-useAuth").change();
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
$("#node-input-ret").change(function() { $("#node-input-ret").change(function() {
if ($("#node-input-ret").val() === "obj") { if ($("#node-input-ret").val() === "obj") {
$("#tip-json").show(); $("#tip-json").show();
@ -133,6 +158,11 @@
$("#tip-json").hide(); $("#tip-json").hide();
} }
}); });
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
} }
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
/** /**
* Copyright 2013, 2015 IBM Corp. * Copyright 2013, 2016 IBM Corp.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,13 +24,16 @@ module.exports = function(RED) {
function HTTPRequest(n) { function HTTPRequest(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
var node = this;
var nodeUrl = n.url; var nodeUrl = n.url;
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1; var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
var nodeMethod = n.method || "GET"; var nodeMethod = n.method || "GET";
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
this.ret = n.ret || "txt"; this.ret = n.ret || "txt";
if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; } if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; }
else { this.reqTimeout = 120000; } else { this.reqTimeout = 120000; }
var node = this;
var prox, noprox; var prox, noprox;
if (process.env.http_proxy != null) { prox = process.env.http_proxy; } if (process.env.http_proxy != null) { prox = process.env.http_proxy; }
@ -54,7 +57,11 @@ module.exports = function(RED) {
} }
// url must start http:// or https:// so assume http:// if not set // url must start http:// or https:// so assume http:// if not set
if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) { if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) {
url = "http://"+url; if (tlsNode) {
url = "https://"+url;
} else {
url = "http://"+url;
}
} }
var method = nodeMethod.toUpperCase() || "GET"; var method = nodeMethod.toUpperCase() || "GET";
@ -133,6 +140,9 @@ module.exports = function(RED) {
} }
else { node.warn("Bad proxy url: "+process.env.http_proxy); } else { node.warn("Bad proxy url: "+process.env.http_proxy); }
} }
if (tlsNode) {
tlsNode.addTLSOptions(opts);
}
var req = ((/^https/.test(urltotest))?https:http).request(opts,function(res) { var req = ((/^https/.test(urltotest))?https:http).request(opts,function(res) {
(node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8'); (node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8');
msg.statusCode = res.statusCode; msg.statusCode = res.statusCode;
@ -172,6 +182,7 @@ module.exports = function(RED) {
req.abort(); req.abort();
}); });
req.on('error',function(err) { req.on('error',function(err) {
node.error(err,msg);
msg.payload = err.toString() + " : " + url; msg.payload = err.toString() + " : " + url;
msg.statusCode = err.code; msg.statusCode = err.code;
node.send(msg); node.send(msg);

View File

@ -123,6 +123,23 @@
"event": "Event name" "event": "Event name"
} }
}, },
"tls": {
"tls": "TLS configuration",
"label": {
"cert": "Certificate",
"key": "Private Key",
"ca": "CA Certificate",
"verify-server-cert":"Verify server certificate"
},
"placeholder": {
"cert":"path to certificate (PEM format)",
"key":"path to private key (PEM format)",
"ca":"path to CA certificate (PEM format)"
},
"error": {
"missing-file": "No certificate/key file provided"
}
},
"exec": { "exec": {
"spawnerr": "Spawn command must be just the command - no spaces or extra parameters", "spawnerr": "Spawn command must be just the command - no spaces or extra parameters",
"badstdout": "Bad STDOUT", "badstdout": "Bad STDOUT",
@ -240,6 +257,7 @@
"keepalive": "Keep alive time (s)", "keepalive": "Keep alive time (s)",
"cleansession": "Use clean session", "cleansession": "Use clean session",
"use-tls": "Enable secure (SSL/TLS) connection", "use-tls": "Enable secure (SSL/TLS) connection",
"tls-config":"TLS Configuration",
"verify-server-cert":"Verify server certificate", "verify-server-cert":"Verify server certificate",
"compatmode": "Use legacy MQTT 3.1 support" "compatmode": "Use legacy MQTT 3.1 support"
}, },
@ -279,7 +297,9 @@
"return": "Return" "return": "Return"
}, },
"setby": "- set by msg.method -", "setby": "- set by msg.method -",
"basicauth": "Use basic authentication?", "basicauth": "Use basic authentication",
"use-tls": "Enable secure (SSL/TLS) connection",
"tls-config":"TLS Configuration",
"utf8": "a UTF-8 string", "utf8": "a UTF-8 string",
"binary": "a binary buffer", "binary": "a binary buffer",
"json": "a parsed JSON object", "json": "a parsed JSON object",