-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -83,6 +102,7 @@
category: 'config',
defaults: {
name: {value:""},
+ certType: {value:"files"},
cert: {value:"", validate: function(v,opt) {
var currentKey = $("#node-config-input-key").val();
if (currentKey === undefined) {
@@ -103,6 +123,8 @@
}
return RED._("node-red:tls.error.invalid-key");
}},
+ p12: {value:""},
+ p12name: {value:""},
ca: {value:""},
certname: {value:""},
keyname: {value:""},
@@ -115,6 +137,7 @@
certdata: {type:"text"},
keydata: {type:"text"},
cadata: {type:"text"},
+ p12data: {type:"text"},
passphrase: {type:"password"}
},
label: function() {
@@ -124,6 +147,21 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
+ $("#node-config-input-certType").on('change',function() {
+ if ($("#node-config-input-certType").val() === "pfx") {
+ $("#node-tls-conf-cer").hide();
+ $("#node-tls-conf-key").hide();
+ $("#node-tls-conf-ca").hide();
+ $("#node-tls-conf-p12").show();
+ }
+ else {
+ $("#node-tls-conf-cer").show();
+ $("#node-tls-conf-key").show();
+ $("#node-tls-conf-ca").show();
+ $("#node-tls-conf-p12").hide();
+ }
+ });
+
function updateFileUpload() {
if ($("#node-config-input-uselocalfiles").is(':checked')) {
$(".tls-config-input-path").show();
@@ -145,9 +183,19 @@
reader.onload = function(event) {
$("#tls-config-"+property+"name").text(filename);
$(filenameInputId).val(filename);
- $(dataInputId).val(event.target.result);
+ if (property === "p12") {
+ $(dataInputId).val(btoa(String.fromCharCode(...new Uint8Array(event.target.result))))
+ }
+ else {
+ $(dataInputId).val(event.target.result);
+ }
+ }
+ if (property === "p12") {
+ reader.readAsArrayBuffer(file);
+ }
+ else {
+ reader.readAsText(file,"UTF-8");
}
- reader.readAsText(file,"UTF-8");
}
$("#node-config-input-certfile" ).on("change", function() {
saveFile("cert", this.files[0]);
@@ -158,6 +206,9 @@
$("#node-config-input-cafile" ).on("change", function() {
saveFile("ca", this.files[0]);
});
+ $("#node-config-input-p12file" ).on("change", function() {
+ saveFile("p12", this.files[0]);
+ });
function clearNameData(prop) {
$("#tls-config-"+prop+"name").text("");
@@ -173,6 +224,9 @@
$("#tls-config-button-ca-clear").on("click", function() {
clearNameData("ca");
});
+ $("#tls-config-button-p12-clear").on("click", function() {
+ clearNameData("p12");
+ });
if (RED.settings.tlsConfigDisableLocalFiles) {
$("#node-config-row-uselocalfiles").hide();
@@ -180,12 +234,13 @@
$("#node-config-row-uselocalfiles").show();
}
// in case paths were set from old TLS config
- if(this.cert || this.key || this.ca) {
+ if (this.cert || this.key || this.ca || this.p12) {
$("#node-config-input-uselocalfiles").prop('checked',true);
}
$("#tls-config-certname").text(this.certname);
$("#tls-config-keyname").text(this.keyname);
$("#tls-config-caname").text(this.caname);
+ $("#tls-config-p12name").text(this.p12name);
updateFileUpload();
},
oneditsave: function() {
@@ -193,10 +248,13 @@
clearNameData("ca");
clearNameData("cert");
clearNameData("key");
- } else {
+ clearNameData("p12");
+ }
+ else {
$("#node-config-input-ca").val("");
$("#node-config-input-cert").val("");
$("#node-config-input-key").val("");
+ $("#node-config-input-p12").val("");
}
}
});
diff --git a/packages/node_modules/@node-red/nodes/core/network/05-tls.js b/packages/node_modules/@node-red/nodes/core/network/05-tls.js
index 888d749fd..3dd3ba8b5 100644
--- a/packages/node_modules/@node-red/nodes/core/network/05-tls.js
+++ b/packages/node_modules/@node-red/nodes/core/network/05-tls.js
@@ -22,15 +22,27 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.valid = true;
this.verifyservercert = n.verifyservercert;
- var certPath = n.cert.trim();
- var keyPath = n.key.trim();
- var caPath = n.ca.trim();
+ var certPath, keyPath, caPath, p12Path;
+ if (n.cert) { certPath = n.cert.trim(); }
+ if (n.key) { keyPath = n.key.trim(); }
+ if (n.ca) { caPath = n.ca.trim(); }
+ if (n.p12) { p12Path = n.p12.trim(); }
+ this.certType = n.certType || "files";
this.servername = (n.servername||"").trim();
this.alpnprotocol = (n.alpnprotocol||"").trim();
- if ((certPath.length > 0) || (keyPath.length > 0) || (caPath.length > 0)) {
-
- if ( (certPath.length > 0) !== (keyPath.length > 0)) {
+ if (this.certType === "pfx" && p12Path && p12Path.length > 0) {
+ try {
+ this.pfx = fs.readFileSync(p12Path);
+ }
+ catch(err) {
+ this.valid = false;
+ this.error(err.toString());
+ return;
+ }
+ }
+ else if ((certPath && certPath.length > 0) || (keyPath && keyPath.length > 0) || (caPath && caPath.length > 0)) {
+ if ( (certPath && certPath.length > 0) !== (keyPath && keyPath.length > 0)) {
this.valid = false;
this.error(RED._("tls.error.missing-file"));
return;
@@ -46,18 +58,21 @@ module.exports = function(RED) {
if (caPath) {
this.ca = fs.readFileSync(caPath);
}
- } catch(err) {
+ }
+ catch(err) {
this.valid = false;
this.error(err.toString());
return;
}
- } else {
+ }
+ else {
if (this.credentials) {
var certData = this.credentials.certdata || "";
var keyData = this.credentials.keydata || "";
var caData = this.credentials.cadata || "";
+ var p12Data = this.credentials.p12data || "";
- if ((certData.length > 0) !== (keyData.length > 0)) {
+ if ((certData.length > 0) !== (keyData.length > 0) && p12Data.length === 0) {
this.valid = false;
this.error(RED._("tls.error.missing-file"));
return;
@@ -72,6 +87,9 @@ module.exports = function(RED) {
if (caData) {
this.ca = caData;
}
+ if (p12Data) {
+ this.pfx = Buffer.from(p12Data, 'base64');
+ }
}
}
}
@@ -80,6 +98,7 @@ module.exports = function(RED) {
certdata: {type:"text"},
keydata: {type:"text"},
cadata: {type:"text"},
+ p12data: {type:"text"},
passphrase: {type:"password"}
},
settings: {
@@ -92,14 +111,21 @@ module.exports = function(RED) {
TLSConfig.prototype.addTLSOptions = function(opts) {
if (this.valid) {
- if (this.key) {
- opts.key = this.key;
+ if (this.certType === "files") {
+ if (this.key) {
+ opts.key = this.key;
+ }
+ if (this.cert) {
+ opts.cert = this.cert;
+ }
+ if (this.ca) {
+ opts.ca = this.ca;
+ }
}
- if (this.cert) {
- opts.cert = this.cert;
- }
- if (this.ca) {
- opts.ca = this.ca;
+ else {
+ if (this.pfx) {
+ opts.pfx = this.pfx;
+ }
}
if (this.credentials && this.credentials.passphrase) {
opts.passphrase = this.credentials.passphrase;
diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
index bc89992e2..ae6d76072 100644
--- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
@@ -204,19 +204,25 @@
"ca": "CA Certificate",
"verify-server-cert": "Verify server certificate",
"servername": "Server Name",
- "alpnprotocol": "ALPN Protocol"
+ "alpnprotocol": "ALPN Protocol",
+ "certtype": "Cert Type",
+ "files": "Individual files",
+ "p12": "pfx or p12",
+ "pfx": "pfx or p12 file"
},
"placeholder": {
"cert": "path to certificate (PEM format)",
"key": "path to private key (PEM format)",
+ "p12": "path to .pfx or .p12 (PKCS12 format)",
"ca": "path to CA certificate (PEM format)",
- "passphrase": "private key passphrase (optional)",
+ "passphrase": "password or passphrase (optional)",
"servername": "for use with SNI",
"alpnprotocol": "for use with ALPN"
},
"error": {
"missing-file": "No certificate/key file provided",
"invalid-cert": "Certificate not specified",
+ "invalid-p12": "pfx/p12 not specified",
"invalid-key": "Private key not specified"
}
},
diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js
index 0df73b970..703fa1576 100644
--- a/test/nodes/core/network/21-httprequest_spec.js
+++ b/test/nodes/core/network/21-httprequest_spec.js
@@ -211,7 +211,7 @@ describe('HTTP Request Node', function() {
} else {
hash = crypto.createHash('md5');
}
-
+
var hex = hash.update(value).digest('hex');
if (algorithm === 'SHA-512-256') {
hex = hex.slice(0, 64);
@@ -1802,10 +1802,37 @@ describe('HTTP Request Node', function() {
})
});
+ it('should use pfx tls file', function(done) {
+ var flow = [
+ {id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslTestURLWithoutProtocol('/text'),tls:"n3"},
+ {id:"n2", type:"helper"},
+ {id:"n3", type:"tls-config", p12:"test/resources/ssl/test.pfx", verifyservercert:false}];
+ var testNodes = [httpRequestNode, tlsNode];
+ helper.load(testNodes, flow, function() {
+ var n3 = helper.getNode("n3");
+ var n2 = helper.getNode("n2");
+ var n1 = helper.getNode("n1");
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property('payload','hello');
+ msg.should.have.property('statusCode',200);
+ msg.should.have.property('headers');
+ msg.headers.should.have.property('content-length',''+('hello'.length));
+ msg.headers.should.have.property('content-type').which.startWith('text/html');
+ msg.should.have.property('responseUrl').which.startWith('https://');
+ done();
+ } catch(err) {
+ done(err);
+ }
+ });
+ n1.receive({payload:"foo"});
+ });
+ });
+
it('should use env var http_proxy', function(done) {
const url = getTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
-
+
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
@@ -1830,7 +1857,7 @@ describe('HTTP Request Node', function() {
it('should use env var https_proxy', function(done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
-
+
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
@@ -1855,7 +1882,7 @@ describe('HTTP Request Node', function() {
it('should not use env var http*_proxy when no_proxy is set', function(done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
-
+
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
diff --git a/test/resources/ssl/test.pfx b/test/resources/ssl/test.pfx
new file mode 100644
index 000000000..519d8a3fc
Binary files /dev/null and b/test/resources/ssl/test.pfx differ