Added SSH Key support.

Ylian Saint-Hilaire committed Aug 12, 2021 at 22:52 UTC 6798415c92324aa4194bc3f9e0f68d99422ce9bb
5 files changed +128 -29
apprelays.js
+71 -13
@@ -338,7 +338,18 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
338 var connectionOptions = { sock: obj.ser }
339 if (typeof obj.username == 'string') { connectionOptions.username = obj.username; delete obj.username; }
340 if (typeof obj.password == 'string') { connectionOptions.password = obj.password; delete obj.password; }
341 - obj.sshClient.connect(connectionOptions);
341 + if (typeof obj.privateKey == 'string') { connectionOptions.privateKey = obj.privateKey; delete obj.privateKey; }
342 + if (typeof obj.privateKeyPass == 'string') { connectionOptions.passphrase = obj.privateKeyPass; delete obj.privateKeyPass; }
343 + try {
344 + obj.sshClient.connect(connectionOptions);
345 + } catch (ex) {
346 + // Exception, this is generally because we did not provide proper credentials. Ask again.
347 + obj.relayActive = false;
348 + delete obj.sshClient;
349 + delete obj.ser.forwardwrite;
350 + obj.close();
351 + return;
352 + }
353
354 // We are all set, start receiving data
355 ws._socket.resume();
@@ -372,6 +383,8 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
383 obj.termSize = msg;
384 obj.username = msg.username;
385 obj.password = msg.password;
386 + obj.privateKey = msg.key;
387 + obj.privateKeyPass = msg.keypass;
388 startRelayConnection();
389 break;
390 }
@@ -472,10 +485,14 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
485 const changed = (node.ssh == null);
486
487 // Check if credentials are the same
475 - if ((typeof node.ssh == 'object') && (node.ssh.u == obj.username) && (node.ssh.p == obj.password)) return;
488 + //if ((typeof node.ssh == 'object') && (node.ssh.u == obj.username) && (node.ssh.p == obj.password)) return; // TODO
489
490 // Save the credentials
478 - node.ssh = { u: obj.username, p: obj.password };
491 + if (obj.password != null) {
492 + node.ssh = { u: obj.username, p: obj.password };
493 + } else if (obj.privateKey != null) {
494 + node.ssh = { u: obj.username, k: obj.privateKey, kp: obj.privateKeyPass };
495 + }
496 parent.parent.db.Set(node);
497
498 // Event node change if needed
@@ -538,7 +555,17 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
555 var connectionOptions = { sock: obj.ser }
556 if (typeof obj.username == 'string') { connectionOptions.username = obj.username; }
557 if (typeof obj.password == 'string') { connectionOptions.password = obj.password; }
541 - obj.sshClient.connect(connectionOptions);
558 + if (typeof obj.privateKey == 'string') { connectionOptions.privateKey = obj.privateKey; }
559 + if (typeof obj.privateKeyPass == 'string') { connectionOptions.passphrase = obj.privateKeyPass; }
560 + try {
561 + obj.sshClient.connect(connectionOptions);
562 + } catch (ex) {
563 + // Exception, this is generally because we did not provide proper credentials. Ask again.
564 + obj.relayActive = false;
565 + delete obj.sshClient;
566 + delete obj.ser.forwardwrite;
567 + try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
568 + }
569
570 // We are all set, start receiving data
571 ws._socket.resume();
@@ -569,13 +596,15 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
596 switch (msg.action) {
597 case 'sshauth': {
598 // Verify inputs
572 - if ((typeof msg.username != 'string') || (typeof msg.password != 'string')) break;
599 + if ((typeof msg.username != 'string') || ((typeof msg.password != 'string') && (typeof msg.key != 'string'))) break;
600 if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break;
601
602 obj.keep = msg.keep; // If true, keep store credentials on the server if the SSH tunnel connected succesfully.
603 obj.termSize = msg;
604 obj.username = msg.username;
605 obj.password = msg.password;
606 + obj.privateKey = msg.key;
607 + obj.privateKeyPass = msg.keypass;
608
609 // Create a mesh relay authentication cookie
610 var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport };
@@ -588,7 +617,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
617 if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break;
618 obj.termSize = msg;
619
591 - if ((obj.username == null) || (obj.password == null)) return;
620 + if ((obj.username == null) || ((obj.password == null) && (obj.privateKey == null))) return;
621
622 // Create a mesh relay authentication cookie
623 var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport };
@@ -639,13 +668,18 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
668 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
669 const node = nodes[0];
670
642 - if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || (typeof node.ssh.p != 'string')) {
671 + if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
672 // Send a request for SSH authentication
673 try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
674 } else {
675 // Use our existing credentials
676 obj.username = node.ssh.u;
648 - obj.password = node.ssh.p;
677 + if (typeof node.ssh.p == 'string') {
678 + obj.password = node.ssh.p;
679 + } else if (typeof node.ssh.k == 'string') {
680 + obj.privateKey = node.ssh.k;
681 + obj.privateKeyPass = node.ssh.kp;
682 + }
683 try { ws.send(JSON.stringify({ action: 'sshautoauth' })) } catch (ex) { }
684 }
685 });
@@ -722,8 +756,15 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
756 const node = nodes[0];
757 const changed = (node.ssh == null);
758
759 + // Check if credentials are the same
760 + //if ((typeof node.ssh == 'object') && (node.ssh.u == obj.username) && (node.ssh.p == obj.password)) return; // TODO
761 +
762 // Save the credentials
726 - node.ssh = { u: obj.username, p: obj.password };
763 + if (obj.password != null) {
764 + node.ssh = { u: obj.username, p: obj.password };
765 + } else if (obj.privateKey != null) {
766 + node.ssh = { u: obj.username, k: obj.privateKey, kp: obj.privateKeyPass };
767 + }
768 parent.parent.db.Set(node);
769
770 // Event node change if needed
@@ -781,7 +822,17 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
822 var connectionOptions = { sock: obj.ser }
823 if (typeof obj.username == 'string') { connectionOptions.username = obj.username; }
824 if (typeof obj.password == 'string') { connectionOptions.password = obj.password; }
784 - obj.sshClient.connect(connectionOptions);
825 + if (typeof obj.privateKey == 'string') { connectionOptions.privateKey = obj.privateKey; }
826 + if (typeof obj.privateKeyPass == 'string') { connectionOptions.passphrase = obj.privateKeyPass; }
827 + try {
828 + obj.sshClient.connect(connectionOptions);
829 + } catch (ex) {
830 + // Exception, this is generally because we did not provide proper credentials. Ask again.
831 + obj.relayActive = false;
832 + delete obj.sshClient;
833 + delete obj.ser.forwardwrite;
834 + try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
835 + }
836
837 // We are all set, start receiving data
838 ws._socket.resume();
@@ -995,11 +1046,13 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
1046 if (obj.sshClient != null) return;
1047
1048 // Verify inputs
998 - if ((typeof msg.username != 'string') || (typeof msg.password != 'string')) break;
1049 + if ((typeof msg.username != 'string') || ((typeof msg.password != 'string') && (typeof msg.key != 'string'))) break;
1050
1051 obj.keep = (msg.keep === true); // If true, keep store credentials on the server if the SSH tunnel connected succesfully.
1052 obj.username = msg.username;
1053 obj.password = msg.password;
1054 + obj.privateKey = msg.key;
1055 + obj.privateKeyPass = msg.keypass;
1056
1057 // Create a mesh relay authentication cookie
1058 var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport };
@@ -1068,13 +1121,18 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
1121 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
1122 const node = nodes[0];
1123
1071 - if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || (typeof node.ssh.p != 'string')) {
1124 + if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
1125 // Send a request for SSH authentication
1126 try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
1127 } else {
1128 // Use our existing credentials
1129 obj.username = node.ssh.u;
1077 - obj.password = node.ssh.p;
1130 + if (typeof node.ssh.p == 'string') {
1131 + obj.password = node.ssh.p;
1132 + } else if (typeof node.ssh.k == 'string') {
1133 + obj.privateKey = node.ssh.k;
1134 + obj.privateKeyPass = node.ssh.kp;
1135 + }
1136
1137 // Create a mesh relay authentication cookie
1138 var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport };
meshuser.js
+1 -1
@@ -717,7 +717,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
717 if (docs[i].pmt != null) { docs[i].pmt = 1; }
718
719 // Remove SSH credentials if present
720 - if (docs[i].ssh != null) { docs[i].ssh = 1; }
720 + if (docs[i].ssh != null) { docs[i].ssh = (docs[i].ssh.k) ? 2 : 1; }
721
722 // Remove RDP credentials if present
723 if (docs[i].rdp != null) { docs[i].rdp = 1; }
views/default-mobile.handlebars
+2 -2
@@ -3439,10 +3439,10 @@
3439 if ((node.ssh != null) || (node.rdp != null)) {
3440 var y = [];
3441 if ((meshrights & 4) != 0) {
3442 - if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
3442 + if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + ((node.ssh == 2) ? "SSH-Key" : "SSH") + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
3443 if (node.rdp != null) { y.push('<span onclick=showClearRdpDialog(3) style=cursor:pointer>' + "RDP" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
3444 } else {
3445 - if (node.ssh != null) { y.push("SSH"); }
3445 + if (node.ssh != null) { y.push(((node.ssh == 2) ? "SSH-Key" : "SSH")); }
3446 if (node.rdp != null) { y.push("RDP"); }
3447 }
3448 x += addDeviceAttribute("Credentials", y.join(', '));
views/default.handlebars
+52 -11
@@ -6624,10 +6624,10 @@
6624 if ((node.ssh != null) || (node.rdp != null)) {
6625 var y = [];
6626 if ((meshrights & 4) != 0) {
6627 - if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
6627 + if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + ((node.ssh == 2)?"SSH-Key":"SSH") + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
6628 if (node.rdp != null) { y.push('<span onclick=showClearRdpDialog(3) style=cursor:pointer>' + "RDP" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
6629 } else {
6630 - if (node.ssh != null) { y.push("SSH"); }
6630 + if (node.ssh != null) { y.push(((node.ssh == 2)?"SSH-Key":"SSH")); }
6631 if (node.rdp != null) { y.push("RDP"); }
6632 }
6633 x += addDeviceAttribute("Credentials", y.join(', '));
@@ -8957,11 +8957,17 @@
8957 switch (j.action) {
8958 case 'sshauth': {
8959 var x = '';
8960 - x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
8961 - x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
8960 + x += addHtmlValue("Authentication", '<select id=dp2authmethod style=width:230px onchange=sshAuthUpdate(event)><option value=1 selected>' + "Username & Password" + '</option><option value=2>' + "Username and Key" + '</option></select>')
8961 + x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
8962 + x += '<div id=d2passauth>';
8963 + x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
8964 + x += '</div><div id=d2keyauth style=display:none>';
8965 + x += addHtmlValue("Key File", '<input type=file id=dp2key style=width:230px maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />');
8966 + x += addHtmlValue("Key Password", '<input type=password id=dp2keypass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
8967 + x += '</div>';
8968 x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
8969 setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh');
8964 - setTimeout(sshAuthKeyUp, 50);
8970 + setTimeout(sshAuthUpdate, 50);
8971 break;
8972 }
8973 case 'sshautoauth': {
@@ -8977,12 +8983,35 @@
8983 }
8984 }
8985
8980 - function sshAuthKeyUp(e) { QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0)); }
8986 + function sshAuthUpdate(e) {
8987 + QV('d2passauth', Q('dp2authmethod').value == 1);
8988 + QV('d2keyauth', Q('dp2authmethod').value == 2);
8989 + if (Q('dp2authmethod').value == 1) {
8990 + QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0));
8991 + } else {
8992 + QE('idx_dlgOkButton', false);
8993 + var ok = (Q('dp2user').value.length > 0) && (Q('dp2key').files != null) && (Q('dp2key').files.length == 1) && (Q('dp2key').files[0].size < 8000);
8994 + if (ok == true) {
8995 + var reader = new FileReader();
8996 + reader.onload = function (e) {
8997 + var validkey = ((e.target.result.indexOf('-----BEGIN OPENSSH PRIVATE KEY-----') >= 0) && (e.target.result.indexOf('-----END OPENSSH PRIVATE KEY-----') >= 0));
8998 + QE('idx_dlgOkButton', validkey);
8999 + }
9000 + reader.readAsText(Q('dp2key').files[0]);
9001 + }
9002 + }
9003 + }
9004 function sshConnectEx(b) {
9005 if (b == 0) {
9006 if (terminal != null) { connectTerminal(); } // Disconnect
9007 } else {
8985 - terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: Q('dp2keep').checked, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
9008 + if (Q('dp2authmethod').value == 1) {
9009 + terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: Q('dp2keep').checked, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
9010 + } else {
9011 + var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value, keep = Q('dp2keep').checked;
9012 + reader.onload = function (e) { terminal.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight })); }
9013 + reader.readAsText(Q('dp2key').files[0]);
9014 + }
9015 }
9016 }
9017
@@ -9312,11 +9341,17 @@
9341 switch (data.action) {
9342 case 'sshauth': {
9343 var x = '';
9315 - x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
9316 - x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
9344 + x += addHtmlValue("Authentication", '<select id=dp2authmethod style=width:230px onchange=sshAuthUpdate(event)><option value=1 selected>' + "Username & Password" + '</option><option value=2>' + "Username and Key" + '</option></select>')
9345 + x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
9346 + x += '<div id=d2passauth>';
9347 + x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
9348 + x += '</div><div id=d2keyauth style=display:none>';
9349 + x += addHtmlValue("Key File", '<input type=file id=dp2key style=width:230px maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />');
9350 + x += addHtmlValue("Key Password", '<input type=password id=dp2keypass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
9351 + x += '</div>';
9352 x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
9353 setDialogMode(2, "Authentication", 11, p13sshConnectEx, x, 'ssh');
9319 - setTimeout(sshAuthKeyUp, 50);
9354 + setTimeout(sshAuthUpdate, 50);
9355 return;
9356 }
9357 case 'autherror': { p13setConsoleMsg("Authentication Error", 5000); return; }
@@ -9367,7 +9402,13 @@
9402 if (b == 0) {
9403 if (files != null) { connectFiles(); } // Disconnect
9404 } else {
9370 - files.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: Q('dp2keep').checked }));
9405 + if (Q('dp2authmethod').value == 1) {
9406 + files.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: Q('dp2keep').checked }));
9407 + } else {
9408 + var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value, keep = Q('dp2keep').checked;
9409 + reader.onload = function (e) { files.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep })); }
9410 + reader.readAsText(Q('dp2key').files[0]);
9411 + }
9412 }
9413 }
9414
webserver.js
+2 -2
@@ -7072,10 +7072,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
7072 obj.CloneSafeNode = function (node) {
7073 if (typeof node != 'object') { return node; }
7074 var r = node;
7075 - if ((r.pmt != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
7075 + if ((r.pmt != null) || (r.ssh != null) || (r.rdp != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
7076 r = Object.assign({}, r); // Shallow clone
7077 if (r.pmt != null) { r.pmt = 1; }
7078 - if (r.ssh != null) { r.ssh = 1; }
7078 + if (r.ssh != null) { r.ssh = (r.ssh.k != null) ? 2 : 1; }
7079 if (r.rdp != null) { r.rdp = 1; }
7080 if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
7081 r.intelamt = Object.assign({}, r.intelamt); // Shallow clone