diff --git a/apprelays.js b/apprelays.js index 30466f73..4931dc4c 100644 --- a/apprelays.js +++ b/apprelays.js @@ -290,6 +290,34 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { delete obj.ws; }; + // Save SSH credentials into device + function saveSshCredentials() { + parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) { + if ((err != null) || (nodes == null) || (nodes.length != 1)) return; + const node = nodes[0]; + const changed = (node.ssh == null); + + // Check if credentials are the same + //if ((typeof node.ssh == 'object') && (node.ssh.u == obj.username) && (node.ssh.p == obj.password)) return; // TODO + + // Save the credentials + if (obj.password != null) { + node.ssh = { u: obj.username, p: obj.password }; + } else if (obj.privateKey != null) { + node.ssh = { u: obj.username, k: obj.privateKey, kp: obj.privateKeyPass }; + } else return; + parent.parent.db.Set(node); + + // Event node change if needed + if (changed) { + // Event the node change + var event = { etype: 'node', action: 'changenode', nodeid: obj.cookie.nodeid, domain: domain.id, userid: obj.cookie.userid, 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.cookie.nodeid]), obj, event); + } + }); + } + // Decode the authentication cookie obj.cookie = parent.parent.decodeCookie(req.query.auth, parent.parent.loginCookieEncryptionKey); if (obj.cookie == null) { obj.ws.send(JSON.stringify({ action: 'sessionerror' })); obj.close(); return; } @@ -317,6 +345,9 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { 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; @@ -327,7 +358,8 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { obj.ws.send(JSON.stringify({ action: 'connected' })); }); 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(); }); @@ -336,10 +368,10 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { // 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.privateKey == 'string') { connectionOptions.privateKey = obj.privateKey; delete obj.privateKey; } - if (typeof obj.privateKeyPass == 'string') { connectionOptions.passphrase = obj.privateKeyPass; delete obj.privateKeyPass; } + if (typeof obj.username == 'string') { connectionOptions.username = obj.username; } + if (typeof obj.password == 'string') { connectionOptions.password = obj.password; } + if (typeof obj.privateKey == 'string') { connectionOptions.privateKey = obj.privateKey; } + if (typeof obj.privateKeyPass == 'string') { connectionOptions.passphrase = obj.privateKeyPass; } try { obj.sshClient.connect(connectionOptions); } catch (ex) { @@ -376,16 +408,41 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { if (typeof msg.action != 'string') return; switch (msg.action) { case 'connect': { - // Verify inputs - 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; + if (msg.useexisting) { + // Check if we have SSH credentials for this device + parent.parent.db.Get(obj.cookie.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') && (typeof node.ssh.k != 'string'))) { + // Send a request for SSH authentication + try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { } + } else { + // Use our existing credentials + obj.termSize = msg; + obj.keep = false; + obj.username = node.ssh.u; + if (typeof node.ssh.p == 'string') { + obj.password = node.ssh.p; + } else if (typeof node.ssh.k == 'string') { + obj.privateKey = node.ssh.k; + obj.privateKeyPass = node.ssh.kp; + } + startRelayConnection(); + } + }); + } else { + // Verify inputs + if ((typeof msg.username != 'string') || ((typeof msg.password != 'string') && (typeof msg.key != 'string'))) break; + if ((typeof msg.rows != 'number') || (typeof msg.cols != 'number') || (typeof msg.height != 'number') || (typeof msg.width != 'number')) break; - obj.termSize = msg; - obj.username = msg.username; - obj.password = msg.password; - obj.privateKey = msg.key; - obj.privateKeyPass = msg.keypass; - startRelayConnection(); + obj.termSize = msg; + obj.keep = msg.keep; // If true, keep store credentials on the server if the SSH tunnel connected succesfully. + obj.username = msg.username; + obj.password = msg.password; + obj.privateKey = msg.key; + obj.privateKeyPass = msg.keypass; + startRelayConnection(); + } break; } case 'resize': { diff --git a/views/default.handlebars b/views/default.handlebars index 357ab29f..a31a6464 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2471,7 +2471,7 @@ } } } - x += addHtmlValue2("Last interfaces update", printDateTime(new Date(message.updateTime))); + if (message.updateTime != null) { x += addHtmlValue2("Last interfaces update", printDateTime(new Date(message.updateTime))); } if (message.netif != null) { // Old style diff --git a/views/ssh.handlebars b/views/ssh.handlebars index 2578a185..f21591c8 100644 --- a/views/ssh.handlebars +++ b/views/ssh.handlebars @@ -92,7 +92,12 @@ // Update the terminal status and buttons updateState(); + resetTerminal(); + connectButton(); + } + + function resetTerminal() { // Setup the terminal with auto-fit if (term != null) { term.dispose(); } if (urlargs.fixsize != 1) { termfit = new FitAddon.FitAddon(); } @@ -107,8 +112,6 @@ resizeTimer = setTimeout(sendResize, 200); }); //term.setOption('convertEol', true); // Consider \n to be \r\n, this should be taken care of by "termios" - - connectButton(); } // Send the new terminal size to the agent @@ -119,24 +122,47 @@ function connectButton() { if (state == 0) { - var x = ''; - x += addHtmlValue("Username", ''); - x += addHtmlValue("Password", ''); - setDialogMode(2, "Authentication", 3, connectEx, x); - Q('dp2user').value = user; - Q('dp2pass').value = pass; - if (user == '') { Q('dp2user').focus(); } else { Q('dp2pass').focus(); } - setTimeout(authKeyUp, 50); + connectEx2({ action: 'connect', cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight, useexisting: true }); } else { disconnect(); } } - function authKeyUp(e) { QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0)); } + function sshAuthUpdate(e) { + QV('d2passauth', Q('dp2authmethod').value == 1); + QV('d2keyauth', Q('dp2authmethod').value == 2); + if (Q('dp2authmethod').value == 1) { + QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0)); + } else { + QE('idx_dlgOkButton', false); + var ok = (Q('dp2user').value.length > 0) && (Q('dp2key').files != null) && (Q('dp2key').files.length == 1) && (Q('dp2key').files[0].size < 8000); + if (ok == true) { + var reader = new FileReader(); + reader.onload = function (e) { + var validkey = ((e.target.result.indexOf('-----BEGIN OPENSSH PRIVATE KEY-----') >= 0) && (e.target.result.indexOf('-----END OPENSSH PRIVATE KEY-----') >= 0)); + QE('idx_dlgOkButton', validkey); + QS('d2badkey')['color'] = validkey ? '#000' : '#F00'; + } + reader.readAsText(Q('dp2key').files[0]); + } + } + } function connectEx() { - user = Q('dp2user').value; - pass = Q('dp2pass').value; + var cmd = { action: 'connect', cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight, username: Q('dp2user').value, keep: Q('dp2keep').checked }; + + if (Q('dp2authmethod').value == 1) { + cmd.password = Q('dp2pass').value; + connectEx2(cmd); + } else { + cmd.keypass = Q('dp2keypass').value; + var reader = new FileReader(); + reader.onload = function (e) { cmd.key = e.target.result; connectEx2(cmd); } + reader.readAsText(Q('dp2key').files[0]); + } + } + + function connectEx2(cmd) { state = 1; var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainurl + 'sshrelay.ashx?auth=' + cookie + (urlargs.key ? ('&key=' + urlargs.key) : ''); socket = new WebSocket(url); @@ -146,7 +172,7 @@ term.reset(); // Send username and terminal width and height - socket.send(JSON.stringify({ action: 'connect', username: user, password: pass, cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight })); + socket.send(JSON.stringify(cmd)); pass = ''; } socket.onmessage = function (data) { @@ -155,8 +181,27 @@ var json = JSON.parse(data.data); switch (json.action) { case 'connected': { state = 3; updateState(); term.focus(); break; } + case 'sshauth': { + var x = ''; + x += addHtmlValue("Authentication", '') + x += addHtmlValue("Username", ''); + x += '
'; + x += addHtmlValue("Password", ''); + x += '
'; + x += addHtmlValue('', ''); + setDialogMode(2, "Authentication", 3, connectEx, x); + Q('dp2user').value = user; + Q('dp2pass').value = pass; + if (user == '') { Q('dp2user').focus(); } else { Q('dp2pass').focus(); } + setTimeout(sshAuthUpdate, 50); + break; + } case 'autherror': { setDialogMode(2, "Authentication", 1, null, "Unable to authenticate."); break; } case 'sessionerror': { setDialogMode(2, "Session", 1, null, "Session expired."); break; } + case 'sessiontimeout': { setDialogMode(2, "Session", 1, null, "Session timeout."); break; } } } else if (data.data[0] == '~') { term.writeUtf8(data.data.substring(1)); @@ -171,6 +216,7 @@ if (socket != null) { socket.close(); socket = null; } state = 0; updateState(); + resetTerminal(); } function updateState() {