SSH remember credentials.
Ylian Saint-Hilaire committed
May 21, 2021 at 13:43 UTC
2416545aa402bc3efce163894f79541b31804e83
4 files changed
+106
-37
apprelays.js
+61
-5
@@ -165,6 +165,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
165
166
// Construct a SSH Relay object, called upon connection
167
module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
168
+ console.log('CreateSshRelay');
169
const Net = require('net');
170
const WebSocket = require('ws');
171
@@ -293,6 +294,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
294
// When data is received from the web socket
295
// SSH default port is 22
296
ws.on('message', function (msg) {
297
+ console.log('message', msg);
298
try {
299
if (typeof msg != 'string') return;
300
if (msg[0] == '{') {
@@ -400,6 +402,28 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
402
delete obj.ws;
403
};
404
405
+ // Save SSH credentials into device
406
+ function saveSshCredentials() {
407
+ console.log('Save SSH credentials', obj.username, obj.password, obj.nodeid);
408
+ parent.parent.db.Get(obj.nodeid, function (err, nodes) {
409
+ if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
410
+ const node = nodes[0];
411
+ const changed = (node.ssh == null);
412
+
413
+ // Save the credentials
414
+ node.ssh = { u: obj.username, p: obj.password };
415
+ parent.parent.db.Set(node);
416
+
417
+ // Event node change if needed
418
+ if (changed) {
419
+ // Event the node change
420
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.nodeid, domain: domain.id, userid: user._id, username: user.name, node: parent.CloneSafeNode(node), msg: "Changed SSH credentials" };
421
+ if (parent.parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
422
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.nodeid]), obj, event);
423
+ }
424
+ });
425
+ }
426
+
427
// Start the looppback server
428
function startRelayConnection(authCookie) {
429
try {
@@ -423,6 +447,9 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
447
const Client = require('ssh2').Client;
448
obj.sshClient = new Client();
449
obj.sshClient.on('ready', function () { // Authentication was successful.
450
+ // If requested, save the credentials
451
+ if (obj.keep === true) saveSshCredentials();
452
+
453
obj.sshClient.shell(function (err, stream) { // Start a remote shell
454
if (err) { obj.close(); return; }
455
obj.sshShell = stream;
@@ -433,7 +460,8 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
460
obj.ws.send('c');
461
});
462
obj.sshClient.on('error', function (err) {
436
- if (err.level == 'client-authentication') { obj.ws.send(JSON.stringify({ action: 'autherror' })); }
463
+ if (err.level == 'client-authentication') { try { obj.ws.send(JSON.stringify({ action: 'autherror' })); } catch (ex) { } }
464
+ if (err.level == 'client-timeout') { try { obj.ws.send(JSON.stringify({ action: 'sessiontimeout' })); } catch (ex) { } }
465
obj.close();
466
});
467
@@ -442,8 +470,8 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
470
471
// Connect the SSH module to the serial tunnel
472
var connectionOptions = { sock: obj.ser }
445
- if (typeof obj.username == 'string') { connectionOptions.username = obj.username; delete obj.username; }
446
- if (typeof obj.password == 'string') { connectionOptions.password = obj.password; delete obj.password; }
473
+ if (typeof obj.username == 'string') { connectionOptions.username = obj.username; }
474
+ if (typeof obj.password == 'string') { connectionOptions.password = obj.password; }
475
obj.sshClient.connect(connectionOptions);
476
477
// We are all set, start receiving data
@@ -475,6 +503,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
503
if ((typeof msg.username != 'string') || (typeof msg.password != 'string')) break;
504
if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break;
505
506
+ obj.keep = msg.keep; // If true, keep store credentials on the server if the SSH tunnel connected succesfully.
507
obj.termSize = msg;
508
obj.username = msg.username;
509
obj.password = msg.password;
@@ -485,6 +514,19 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
514
startRelayConnection(parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey));
515
break;
516
}
517
+ case 'sshautoauth': {
518
+ // Verify inputs
519
+ if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break;
520
+ obj.termSize = msg;
521
+
522
+ if ((obj.username == null) || (obj.password == null)) return;
523
+
524
+ // Create a mesh relay authentication cookie
525
+ var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport };
526
+ if (obj.mtype == 3) { cookieContent.lc = 1; } // This is a local device
527
+ startRelayConnection(parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey));
528
+ break;
529
+ }
530
case 'resize': {
531
// Verify inputs
532
if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break;
@@ -531,8 +573,22 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
573
// We are all set, start receiving data
574
ws._socket.resume();
575
534
- // Send a request for SSH authentication
535
- try { ws.send(JSON.stringify({ action:'sshauth' })) } catch (ex) { }
576
+ // Check if we have SSH credentials for this device
577
+ parent.parent.db.Get(obj.nodeid, function (err, nodes) {
578
+ if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
579
+ const node = nodes[0];
580
+
581
+ if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || (typeof node.ssh.p != 'string')) {
582
+ // Send a request for SSH authentication
583
+ try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
584
+ } else {
585
+ // Use our existing credentials
586
+ obj.username = node.ssh.u;
587
+ obj.password = node.ssh.p;
588
+ try { ws.send(JSON.stringify({ action: 'sshautoauth' })) } catch (ex) { }
589
+ }
590
+ });
591
+
592
});
593
594
return obj;
views/default-mobile.handlebars
+21
-11
@@ -3999,6 +3999,17 @@
3999
function p12clearConsoleMsg() { QH('p12TermConsoleMsg', ''); QV('p12TermConsoleMsg', false); if (p12TermConsoleMsgTimer) { clearTimeout(p12TermConsoleMsgTimer); p12TermConsoleMsgTimer = null; } }
4000
function p13clearConsoleMsg() { QH('p13FilesConsoleMsg', ''); QV('p13FilesConsoleMsg', false); if (p13FilesConsoleMsgTimer) { clearTimeout(p13FilesConsoleMsgTimer); p13FilesConsoleMsgTimer = null; } }
4001
4002
+ function p12setConsoleMsg(msg, timeout) {
4003
+ if (msg) {
4004
+ Q('p12TermConsoleMsg').innerHTML += msg;
4005
+ QV('p12TermConsoleMsg', true);
4006
+ if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
4007
+ if (timeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, timeout); }
4008
+ } else {
4009
+ p12clearConsoleMsg();
4010
+ }
4011
+ }
4012
+
4013
function onDesktopStateChange(xdesktop, state) {
4014
var xstate = state;
4015
if ((xstate == 3) && (xdesktop.contype == 2)) { xstate++; }
@@ -4567,9 +4578,17 @@
4578
var x = '';
4579
x += addHtmlValue("Username", '<input id=dp2user style=width:190px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
4580
x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:190px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
4581
+ x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
4582
setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh');
4583
setTimeout(sshAuthKeyUp, 50);
4584
}
4585
+ case 'sshautoauth': {
4586
+ terminal.socket.send(JSON.stringify({ action: 'sshautoauth', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
4587
+ break;
4588
+ }
4589
+ case 'autherror': { p12setConsoleMsg("Authentication Error", 5000); break; }
4590
+ case 'sessionerror': { p12setConsoleMsg("Session expired", 5000); break; }
4591
+ case 'sessiontimeout': { p12setConsoleMsg("Session timeout", 5000); break; }
4592
}
4593
} else if (data[0] == '~') { xterm.writeUtf8(data.substring(1)); }
4594
}
@@ -4580,7 +4599,7 @@
4599
if (b == 0) {
4600
if (terminal != null) { connectTerminal(); } // Disconnect
4601
} else {
4583
- terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
4602
+ 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 }));
4603
}
4604
}
4605
@@ -4648,16 +4667,7 @@
4667
terminal.onStateChanged = onTerminalStateChange;
4668
terminal.contype = 1;
4669
terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it.
4651
- terminal.onConsoleMessageChange = function (server, msg) {
4652
- if (terminal.consoleMessage) {
4653
- Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs);
4654
- QV('p12TermConsoleMsg', true);
4655
- if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
4656
- if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); }
4657
- } else {
4658
- p12clearConsoleMsg();
4659
- }
4660
- };
4670
+ terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); }
4671
} else {
4672
terminal.Stop();
4673
terminal = null;
views/default.handlebars
+23
-21
@@ -7804,6 +7804,17 @@
7804
function p12clearConsoleMsg() { QH('p12TermConsoleMsg', ''); QV('p12TermConsoleMsg', false); if (p12TermConsoleMsgTimer) { clearTimeout(p12TermConsoleMsgTimer); p12TermConsoleMsgTimer = null; } }
7805
function p13clearConsoleMsg() { QH('p13FilesConsoleMsg', ''); QV('p13FilesConsoleMsg', false); if (p13FilesConsoleMsgTimer) { clearTimeout(p13FilesConsoleMsgTimer); p13FilesConsoleMsgTimer = null; } }
7806
7807
+ function p12setConsoleMsg(msg, timeout) {
7808
+ if (msg) {
7809
+ Q('p12TermConsoleMsg').innerHTML += msg;
7810
+ QV('p12TermConsoleMsg', true);
7811
+ if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
7812
+ if (timeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, timeout); }
7813
+ } else {
7814
+ p12clearConsoleMsg();
7815
+ }
7816
+ }
7817
+
7818
var webRtcDesktop = null;
7819
function webRtcDesktopReset() {
7820
if (webRtcDesktop == null) return;
@@ -8680,9 +8691,18 @@
8691
var x = '';
8692
x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
8693
x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthKeyUp(event) />');
8694
+ x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
8695
setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh');
8696
setTimeout(sshAuthKeyUp, 50);
8697
+ break;
8698
+ }
8699
+ case 'sshautoauth': {
8700
+ terminal.socket.send(JSON.stringify({ action: 'sshautoauth', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
8701
+ break;
8702
}
8703
+ case 'autherror': { p12setConsoleMsg("Authentication Error", 5000); break; }
8704
+ case 'sessionerror': { p12setConsoleMsg("Session expired", 5000); break; }
8705
+ case 'sessiontimeout': { p12setConsoleMsg("Session timeout", 5000); break; }
8706
}
8707
} else if (data[0] == '~') { xterm.writeUtf8(data.substring(1)); }
8708
}
@@ -8693,7 +8713,7 @@
8713
if (b == 0) {
8714
if (terminal != null) { connectTerminal(); } // Disconnect
8715
} else {
8696
- terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
8716
+ 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 }));
8717
}
8718
}
8719
@@ -8796,16 +8816,7 @@
8816
terminal.onStateChanged = onTerminalStateChange;
8817
terminal.contype = 1;
8818
terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it.
8799
- terminal.onConsoleMessageChange = function (server, msg) {
8800
- if (terminal.consoleMessage) {
8801
- Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs);
8802
- QV('p12TermConsoleMsg', true);
8803
- if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
8804
- if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); }
8805
- } else {
8806
- p12clearConsoleMsg();
8807
- }
8808
- };
8819
+ terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); }
8820
} else {
8821
QV('termarea3xdiv', false);
8822
QV('Term', true);
@@ -8819,16 +8830,7 @@
8830
terminal.m.lineFeed = ([1, 2, 3, 4, 21, 22].indexOf(currentNode.agent.id) >= 0) ? '\r\n' : '\r'; // On windows, send \r\n, on Linux only \r
8831
terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it.
8832
terminal.onStateChanged = onTerminalStateChange;
8822
- terminal.onConsoleMessageChange = function () {
8823
- if (terminal.consoleMessage) {
8824
- Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs);
8825
- QV('p12TermConsoleMsg', true);
8826
- if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
8827
- if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); }
8828
- } else {
8829
- p12clearConsoleMsg();
8830
- }
8831
- }
8833
+ terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); }
8834
terminal.Start(terminalNode._id);
8835
terminal.contype = 1;
8836
terminal.m.terminalEmulation = 0;
webserver.js
+1
@@ -6898,6 +6898,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6898
if ((r.pmt != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
6899
r = Object.assign({}, r); // Shallow clone
6900
if (r.pmt != null) { r.pmt = 1; }
6901
+ if (r.ssh != null) { r.ssh = 1; }
6902
if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
6903
r.intelamt = Object.assign({}, r.intelamt); // Shallow clone
6904
if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node