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