From 2416545aa402bc3efce163894f79541b31804e83 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 21 May 2021 13:43:32 -0700 Subject: [PATCH] SSH remember credentials. --- apprelays.js | 66 ++++++++++++++++++++++++++++++--- views/default-mobile.handlebars | 32 ++++++++++------ views/default.handlebars | 44 +++++++++++----------- webserver.js | 1 + 4 files changed, 106 insertions(+), 37 deletions(-) diff --git a/apprelays.js b/apprelays.js index 69fed7fe..003a8629 100644 --- a/apprelays.js +++ b/apprelays.js @@ -165,6 +165,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { // Construct a SSH Relay object, called upon connection module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { + console.log('CreateSshRelay'); const Net = require('net'); const WebSocket = require('ws'); @@ -293,6 +294,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { // When data is received from the web socket // SSH default port is 22 ws.on('message', function (msg) { + console.log('message', msg); try { if (typeof msg != 'string') return; if (msg[0] == '{') { @@ -400,6 +402,28 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u delete obj.ws; }; + // Save SSH credentials into device + function saveSshCredentials() { + console.log('Save SSH credentials', obj.username, obj.password, obj.nodeid); + parent.parent.db.Get(obj.nodeid, function (err, nodes) { + if ((err != null) || (nodes == null) || (nodes.length != 1)) return; + const node = nodes[0]; + const changed = (node.ssh == null); + + // Save the credentials + node.ssh = { u: obj.username, p: obj.password }; + parent.parent.db.Set(node); + + // Event node change if needed + if (changed) { + // Event the node change + 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" }; + 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. + parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.nodeid]), obj, event); + } + }); + } + // Start the looppback server function startRelayConnection(authCookie) { try { @@ -423,6 +447,9 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u const Client = require('ssh2').Client; obj.sshClient = new Client(); obj.sshClient.on('ready', function () { // Authentication was successful. + // If requested, save the credentials + if (obj.keep === true) saveSshCredentials(); + obj.sshClient.shell(function (err, stream) { // Start a remote shell if (err) { obj.close(); return; } obj.sshShell = stream; @@ -433,7 +460,8 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u obj.ws.send('c'); }); obj.sshClient.on('error', function (err) { - if (err.level == 'client-authentication') { obj.ws.send(JSON.stringify({ action: 'autherror' })); } + if (err.level == 'client-authentication') { try { obj.ws.send(JSON.stringify({ action: 'autherror' })); } catch (ex) { } } + if (err.level == 'client-timeout') { try { obj.ws.send(JSON.stringify({ action: 'sessiontimeout' })); } catch (ex) { } } obj.close(); }); @@ -442,8 +470,8 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u // Connect the SSH module to the serial tunnel var connectionOptions = { sock: obj.ser } - if (typeof obj.username == 'string') { connectionOptions.username = obj.username; delete obj.username; } - if (typeof obj.password == 'string') { connectionOptions.password = obj.password; delete obj.password; } + if (typeof obj.username == 'string') { connectionOptions.username = obj.username; } + if (typeof obj.password == 'string') { connectionOptions.password = obj.password; } obj.sshClient.connect(connectionOptions); // We are all set, start receiving data @@ -475,6 +503,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u if ((typeof msg.username != 'string') || (typeof msg.password != 'string')) break; if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break; + obj.keep = msg.keep; // If true, keep store credentials on the server if the SSH tunnel connected succesfully. obj.termSize = msg; obj.username = msg.username; obj.password = msg.password; @@ -485,6 +514,19 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u startRelayConnection(parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey)); break; } + case 'sshautoauth': { + // Verify inputs + if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break; + obj.termSize = msg; + + if ((obj.username == null) || (obj.password == null)) return; + + // Create a mesh relay authentication cookie + var cookieContent = { userid: user._id, domainid: user.domain, nodeid: obj.nodeid, tcpport: obj.tcpport }; + if (obj.mtype == 3) { cookieContent.lc = 1; } // This is a local device + startRelayConnection(parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey)); + break; + } case 'resize': { // Verify inputs 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 // We are all set, start receiving data ws._socket.resume(); - // Send a request for SSH authentication - try { ws.send(JSON.stringify({ action:'sshauth' })) } catch (ex) { } + // Check if we have SSH credentials for this device + parent.parent.db.Get(obj.nodeid, function (err, nodes) { + if ((err != null) || (nodes == null) || (nodes.length != 1)) return; + const node = nodes[0]; + + if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || (typeof node.ssh.p != 'string')) { + // Send a request for SSH authentication + try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { } + } else { + // Use our existing credentials + obj.username = node.ssh.u; + obj.password = node.ssh.p; + try { ws.send(JSON.stringify({ action: 'sshautoauth' })) } catch (ex) { } + } + }); + }); return obj; diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index e8539f2f..0c05d199 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -3999,6 +3999,17 @@ function p12clearConsoleMsg() { QH('p12TermConsoleMsg', ''); QV('p12TermConsoleMsg', false); if (p12TermConsoleMsgTimer) { clearTimeout(p12TermConsoleMsgTimer); p12TermConsoleMsgTimer = null; } } function p13clearConsoleMsg() { QH('p13FilesConsoleMsg', ''); QV('p13FilesConsoleMsg', false); if (p13FilesConsoleMsgTimer) { clearTimeout(p13FilesConsoleMsgTimer); p13FilesConsoleMsgTimer = null; } } + function p12setConsoleMsg(msg, timeout) { + if (msg) { + Q('p12TermConsoleMsg').innerHTML += msg; + QV('p12TermConsoleMsg', true); + if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); } + if (timeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, timeout); } + } else { + p12clearConsoleMsg(); + } + } + function onDesktopStateChange(xdesktop, state) { var xstate = state; if ((xstate == 3) && (xdesktop.contype == 2)) { xstate++; } @@ -4567,9 +4578,17 @@ var x = ''; x += addHtmlValue("Username", ''); x += addHtmlValue("Password", ''); + x += addHtmlValue('', ''); setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh'); setTimeout(sshAuthKeyUp, 50); } + case 'sshautoauth': { + terminal.socket.send(JSON.stringify({ action: 'sshautoauth', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight })); + break; + } + case 'autherror': { p12setConsoleMsg("Authentication Error", 5000); break; } + case 'sessionerror': { p12setConsoleMsg("Session expired", 5000); break; } + case 'sessiontimeout': { p12setConsoleMsg("Session timeout", 5000); break; } } } else if (data[0] == '~') { xterm.writeUtf8(data.substring(1)); } } @@ -4580,7 +4599,7 @@ if (b == 0) { if (terminal != null) { connectTerminal(); } // Disconnect } else { - 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 })); + 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 })); } } @@ -4648,16 +4667,7 @@ terminal.onStateChanged = onTerminalStateChange; terminal.contype = 1; terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it. - terminal.onConsoleMessageChange = function (server, msg) { - if (terminal.consoleMessage) { - Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs); - QV('p12TermConsoleMsg', true); - if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); } - if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); } - } else { - p12clearConsoleMsg(); - } - }; + terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); } } else { terminal.Stop(); terminal = null; diff --git a/views/default.handlebars b/views/default.handlebars index 21411d70..e73c2270 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -7804,6 +7804,17 @@ function p12clearConsoleMsg() { QH('p12TermConsoleMsg', ''); QV('p12TermConsoleMsg', false); if (p12TermConsoleMsgTimer) { clearTimeout(p12TermConsoleMsgTimer); p12TermConsoleMsgTimer = null; } } function p13clearConsoleMsg() { QH('p13FilesConsoleMsg', ''); QV('p13FilesConsoleMsg', false); if (p13FilesConsoleMsgTimer) { clearTimeout(p13FilesConsoleMsgTimer); p13FilesConsoleMsgTimer = null; } } + function p12setConsoleMsg(msg, timeout) { + if (msg) { + Q('p12TermConsoleMsg').innerHTML += msg; + QV('p12TermConsoleMsg', true); + if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); } + if (timeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, timeout); } + } else { + p12clearConsoleMsg(); + } + } + var webRtcDesktop = null; function webRtcDesktopReset() { if (webRtcDesktop == null) return; @@ -8680,9 +8691,18 @@ var x = ''; x += addHtmlValue("Username", ''); x += addHtmlValue("Password", ''); + x += addHtmlValue('', ''); setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh'); setTimeout(sshAuthKeyUp, 50); + break; } + case 'sshautoauth': { + terminal.socket.send(JSON.stringify({ action: 'sshautoauth', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight })); + break; + } + case 'autherror': { p12setConsoleMsg("Authentication Error", 5000); break; } + case 'sessionerror': { p12setConsoleMsg("Session expired", 5000); break; } + case 'sessiontimeout': { p12setConsoleMsg("Session timeout", 5000); break; } } } else if (data[0] == '~') { xterm.writeUtf8(data.substring(1)); } } @@ -8693,7 +8713,7 @@ if (b == 0) { if (terminal != null) { connectTerminal(); } // Disconnect } else { - 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 })); + 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 })); } } @@ -8796,16 +8816,7 @@ terminal.onStateChanged = onTerminalStateChange; terminal.contype = 1; terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it. - terminal.onConsoleMessageChange = function (server, msg) { - if (terminal.consoleMessage) { - Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs); - QV('p12TermConsoleMsg', true); - if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); } - if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); } - } else { - p12clearConsoleMsg(); - } - }; + terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); } } else { QV('termarea3xdiv', false); QV('Term', true); @@ -8819,16 +8830,7 @@ 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 terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it. terminal.onStateChanged = onTerminalStateChange; - terminal.onConsoleMessageChange = function () { - if (terminal.consoleMessage) { - Q('p12TermConsoleMsg').innerHTML += formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs); - QV('p12TermConsoleMsg', true); - if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); } - if (terminal.consoleMessageTimeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, terminal.consoleMessageTimeout * 1000); } - } else { - p12clearConsoleMsg(); - } - } + terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); } terminal.Start(terminalNode._id); terminal.contype = 1; terminal.m.terminalEmulation = 0; diff --git a/webserver.js b/webserver.js index c3f1cb35..5e74c398 100644 --- a/webserver.js +++ b/webserver.js @@ -6898,6 +6898,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((r.pmt != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) { r = Object.assign({}, r); // Shallow clone if (r.pmt != null) { r.pmt = 1; } + if (r.ssh != null) { r.ssh = 1; } if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) { r.intelamt = Object.assign({}, r.intelamt); // Shallow clone if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node