diff --git a/ssh.js b/ssh.js index 0ba424ba..0d52e373 100644 --- a/ssh.js +++ b/ssh.js @@ -21,13 +21,12 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { var obj = {}; obj.domain = domain; obj.ws = ws; - obj.wsClient = null; - obj.tcpServer = null; - obj.tcpServerPort = 0; obj.relaySocket = null; obj.relayActive = false; obj.infos = null; - var sshClient = null; + obj.sshClient = null; + obj.sshShell = null; + obj.termSize = null; parent.parent.debug('relay', 'SSH: Request for SSH relay (' + req.clientIp + ')'); @@ -35,102 +34,75 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { obj.close = function (arg) { if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (e) { console.log(e); } } // Soft close, close the websocket if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket - if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; } - if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; } - if (sshClient) { sshClient.close(); sshClient = null; } + //if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; } + //if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; } + //if (sshClient) { sshClient.close(); sshClient = null; } + + if (obj.sshClient != null) { + try { obj.sshClient.end(); } catch (ex) { console.log(ex); } + delete obj.sshClient; + } + if (obj.sshShell != null) { + try { obj.sshShell.end(); } catch (ex) { console.log(ex); } + delete obj.sshShell; + } + delete obj.domain; delete obj.ws; }; - // Start the looppback server - function startTcpServer() { - obj.tcpServer = new Net.Server(); - obj.tcpServer.listen(0, '127.0.0.1', function () { obj.tcpServerPort = obj.tcpServer.address().port; startSSH(obj.tcpServerPort); }); - obj.tcpServer.on('connection', function (socket) { - if (obj.relaySocket != null) { - socket.close(); - } else { - obj.relaySocket = socket; - obj.relaySocket.pause(); - obj.relaySocket.on('data', function (chunk) { // Make sure to handle flow control. - if (obj.relayActive == true) { obj.relaySocket.pause(); obj.wsClient.send(chunk, function () { obj.relaySocket.resume(); }); } - }); - obj.relaySocket.on('end', function () { obj.close(); }); - obj.relaySocket.on('error', function (err) { obj.close(); }); - - // Decode the authentication cookie - var cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey); - if (cookie == null) return; - - // Setup the correct URL with domain and use TLS only if needed. - var options = { rejectUnauthorized: false }; - if (domain.dns != null) { options.servername = domain.dns; } - var protocol = 'wss'; - if (args.tlsoffload) { protocol = 'ws'; } - var domainadd = ''; - if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' } - var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((cookie.lc == 1)?'local':'mesh') + 'relay.ashx?noping=1&auth=' + obj.infos.ip; - parent.parent.debug('relay', 'SSH: Connection websocket to ' + url); - obj.wsClient = new WebSocket(url, options); - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'SSH: Relay websocket open'); }); - obj.wsClient.on('message', function (data) { // Make sure to handle flow control. - if ((obj.relayActive == false) && (data == 'c')) { - obj.relayActive = true; obj.relaySocket.resume(); - } else { - obj.wsClient._socket.pause(); - obj.relaySocket.write(data, function () { obj.wsClient._socket.resume(); }); - } - }); - obj.wsClient.on('close', function () { parent.parent.debug('relay', 'SSH: Relay websocket closed'); obj.close(); }); - obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'SSH: Relay websocket error: ' + err); obj.close(); }); - obj.tcpServer.close(); - obj.tcpServer = null; - } - }); - } - - // Start the SSH client - function startSSH(port) { - parent.parent.debug('relay', 'SSH: Starting SSH client on loopback port ' + port); - try { - sshClient = require('node-rdpjs-2').createClient({ - logLevel: 'ERROR', - domain: obj.infos.domain, - userName: obj.infos.username, - password: obj.infos.password, - enablePerf: true, - autoLogin: true, - screen: obj.infos.screen, - locale: obj.infos.locale - }).on('connect', function () { - send(['rdp-connect']); - }).on('bitmap', function (bitmap) { - try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary - delete bitmap.data; - send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data. - }).on('close', function () { - send(['rdp-close']); - }).on('error', function (err) { - send(['rdp-error', err]); - }).connect('127.0.0.1', obj.tcpServerPort); - } catch (ex) { - console.log('startSshException', ex); - obj.close(); - } - } - // When data is received from the web socket // SSH default port is 22 ws.on('message', function (msg) { try { - msg = JSON.parse(msg); - switch (msg[0]) { - case 'infos': { obj.infos = msg[1]; startTcpServer(); break; } - case 'mouse': { if (sshClient) { sshClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; } - case 'wheel': { if (sshClient) { sshClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; } - case 'scancode': { if (sshClient) { sshClient.sendKeyEventScancode(msg[1], msg[2]); } break; } - case 'unicode': { if (sshClient) { sshClient.sendKeyEventUnicode(msg[1], msg[2]); } break; } - case 'disconnect': { obj.close(); break; } + if (typeof msg != 'string') return; + if (msg[0] == '{') { + // Control data + msg = JSON.parse(msg); + if (typeof msg.action != 'string') return; + switch (msg.action) { + case 'connect': { + obj.termSize = msg; + const Client = require('ssh2').Client; + obj.sshClient = new Client(); + + obj.sshClient.on('ready', function () { // Authentication was successful. + obj.sshClient.shell(function (err, stream) { + if (err) { obj.close(); return; } + obj.sshShell = stream; + obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width); + obj.sshShell.on('close', function () { obj.close(); }); + obj.sshShell.on('data', function (data) { obj.ws.send('~' + data); }); + }); + 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' })); } + obj.close(); + }); + + var connectionOptions = { + //debug: function (msg) { console.log(msg); }, + // sock: // TODO + host: '192.168.2.205', + port: 22 + } + + if (typeof msg.username == 'string') { connectionOptions.username = msg.username; } + if (typeof msg.password == 'string') { connectionOptions.password = msg.password; } + + obj.sshClient.connect(connectionOptions); + break; + } + case 'resize': { + obj.termSize = msg; + if (obj.sshShell != null) { obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width); } + break; + } + } + } else if (msg[0] == '~') { + // Terminal data + if (obj.sshShell != null) { obj.sshShell.write(msg.substring(1)); } } } catch (ex) { console.log('SSHMessageException', msg, ex); @@ -146,8 +118,10 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { // Send an object with flow control function send(obj) { - try { sshClient.bufferLayer.socket.pause(); } catch (ex) { } - try { ws.send(JSON.stringify(obj), function () { try { sshClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { } + //try { sshClient.bufferLayer.socket.pause(); } catch (ex) { } + //try { ws.send(JSON.stringify(obj), function () { try { sshClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { } + + try { ws.send(JSON.stringify(obj), function () { }); } catch (ex) { } } // We are all set, start receiving data diff --git a/translate/translate.json b/translate/translate.json index 30bcc3a5..f6cf8a7a 100644 --- a/translate/translate.json +++ b/translate/translate.json @@ -653,7 +653,6 @@ "sharing.handlebars->11->19", "sharing.handlebars->11->27", "sharing.handlebars->11->44", - "ssh.handlebars->9->6", "terminal.handlebars->3->9", "xterm.handlebars->9->6" ] @@ -4363,7 +4362,6 @@ "zh-cht": "管理員PowerShell", "xloc": [ "default.handlebars->termShellContextMenu->3", - "ssh.handlebars->termShellContextMenu->cxtermps", "xterm.handlebars->termShellContextMenu->cxtermps" ] }, @@ -4405,7 +4403,6 @@ "zh-cht": "管理控制台", "xloc": [ "default.handlebars->termShellContextMenu->1->0", - "ssh.handlebars->termShellContextMenu->cxtermnorm->0", "xterm.handlebars->termShellContextMenu->cxtermnorm->0" ] }, @@ -9704,7 +9701,7 @@ "default.handlebars->31->11", "desktop.handlebars->3->4", "sharing.handlebars->11->4", - "ssh.handlebars->9->4", + "ssh.handlebars->3->4", "terminal.handlebars->3->4", "xterm.handlebars->9->4" ] @@ -9818,7 +9815,7 @@ "desktop.handlebars->3->2", "sharing.handlebars->11->2", "sharing.handlebars->11->93", - "ssh.handlebars->9->2", + "ssh.handlebars->3->2", "terminal.handlebars->3->2", "xterm.handlebars->9->2" ] @@ -13384,7 +13381,7 @@ "sharing.handlebars->p11->deskarea0->deskarea1->3->deskstatus", "sharing.handlebars->p12->5->3->termstatus", "sharing.handlebars->p13->p13toolbar->1->3->p13Status", - "ssh.handlebars->9->1", + "ssh.handlebars->3->1", "terminal.handlebars->3->1", "terminal.handlebars->p12->5->3->termstatus", "xterm.handlebars->9->1" @@ -17987,6 +17984,9 @@ "default.handlebars->31->106" ] }, + { + "en": "Geen Intel® AMT apparaten in deze apparaatgroep" + }, { "cs": "Obecné", "de": "Allgemein", @@ -20275,7 +20275,6 @@ "default.handlebars->31->12", "desktop.handlebars->3->5", "sharing.handlebars->11->5", - "ssh.handlebars->9->5", "terminal.handlebars->3->5", "xterm.handlebars->9->5" ] @@ -23925,7 +23924,6 @@ "zh-cht": "登入控制台", "xloc": [ "default.handlebars->termShellContextMenuLinux->5", - "ssh.handlebars->termShellContextMenuLinux->cxtermps", "xterm.handlebars->termShellContextMenuLinux->cxtermps" ] }, @@ -27216,7 +27214,6 @@ }, { "en": "No Intel® AMT devices in this device group", - "en": "Geen Intel® AMT apparaten in deze apparaatgroep", "xloc": [ "default.handlebars->31->262" ] @@ -33820,7 +33817,6 @@ "zh-cht": "Root Shell", "xloc": [ "default.handlebars->termShellContextMenuLinux->1->0", - "ssh.handlebars->termShellContextMenuLinux->cxtermnorm->0", "xterm.handlebars->termShellContextMenuLinux->cxtermnorm->0" ] }, @@ -36413,7 +36409,7 @@ "default.handlebars->31->331", "desktop.handlebars->3->3", "sharing.handlebars->11->3", - "ssh.handlebars->9->3", + "ssh.handlebars->3->3", "terminal.handlebars->3->3", "xterm.handlebars->9->3" ] @@ -42715,7 +42711,6 @@ "zh-cht": "用戶PowerShell", "xloc": [ "default.handlebars->termShellContextMenu->7", - "ssh.handlebars->termShellContextMenu->cxtermups", "xterm.handlebars->termShellContextMenu->cxtermups" ] }, @@ -42758,8 +42753,6 @@ "xloc": [ "default.handlebars->termShellContextMenu->5", "default.handlebars->termShellContextMenuLinux->3", - "ssh.handlebars->termShellContextMenu->cxtermunorm", - "ssh.handlebars->termShellContextMenuLinux->cxtermps", "xterm.handlebars->termShellContextMenu->cxtermunorm", "xterm.handlebars->termShellContextMenuLinux->cxtermps" ] @@ -47962,4 +47955,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/views/ssh.handlebars b/views/ssh.handlebars index c315308a..bb7949f1 100644 --- a/views/ssh.handlebars +++ b/views/ssh.handlebars @@ -80,6 +80,8 @@ var StatusStrs = ["Disconnected", "Connecting...", "Setup...", "Connected"]; var state = 0; var socket = null; + var user = ''; + var pass = ''; function start() { // When the user resizes the window, re-fit @@ -94,9 +96,7 @@ term = new Terminal(); if (termfit) { term.loadAddon(termfit); } term.open(Q('terminal')); - term.onData(function (data) { - //if (tunnel != null) { tunnel.sendText(data); } - }); + term.onData(function (data) { if (state == 3) { socket.send('~' + data); } }); if (termfit) { termfit.fit(); } term.onResize(function (size) { // Despam resize @@ -109,27 +109,67 @@ // Send the new terminal size to the agent function sendResize() { resizeTimer = null; - //if ((term != null) && (tunnel != null)) { tunnel.sendCtrlMsg(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: term.cols, rows: term.rows })); } + if (socket != null) { socket.send(JSON.stringify({ action: 'resize', cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight })); } } function connectButton() { if (state == 0) { - state = 1; - var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainurl + 'ssh/relay.ashx?auth=' + cookie + (urlargs.key ? ('&key=' + urlargs.key) : ''); - console.log('Connecting to ' + url); - socket = new WebSocket(url); - socket.onopen = function (e) { console.log('open'); state = 2; updateState(); } - socket.onmessage = function (e) { console.log('message'); } - socket.onclose = function (e) { console.log('close'); disconnect(); } - socket.onerror = function (e) { console.log('error'); disconnect(); } - updateState(); + 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); } else { disconnect(); } } + function authKeyUp(e) { QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0)); } + + function connectEx() { + user = Q('dp2user').value; + pass = Q('dp2pass').value; + state = 1; + var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainurl + 'ssh/relay.ashx?auth=' + cookie + (urlargs.key ? ('&key=' + urlargs.key) : ''); + socket = new WebSocket(url); + socket.onopen = function (e) { + state = 2; + updateState(); + 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 })); + pass = ''; + } + socket.onmessage = function (data) { + if (typeof data.data != 'string') return; + if (data.data[0] == '{') { + var json = JSON.parse(data.data); + switch (json.action) { + case 'connected': { + state = 3; + updateState(); + term.focus(); + break; + } + case 'autherror': { + setDialogMode(2, "Authentication", 1, null, "Unable to authenticate."); + break; + } + } + } else if (data.data[0] == '~') { + term.writeUtf8(data.data.substring(1)); + } + } + socket.onclose = function (e) { disconnect(); } + socket.onerror = function (e) { disconnect(); } + updateState(); + } + function disconnect() { - console.log('disconnect'); if (socket != null) { socket.close(); socket = null; } state = 0; updateState(); @@ -186,6 +226,7 @@ function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); }; function isAlphaNumeric(str) { return (str.match(/^[A-Za-z0-9]+$/) != null); }; function isSafeString(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf('%') == -1) && (str.indexOf(':') == -1)) }; + function addHtmlValue(t, v) { return '
' + t + '' + v + '
'; } // Parse URL arguments, only keep safe values function parseUriArgs() { diff --git a/webserver.js b/webserver.js index 6f1dc37a..f4fac90f 100644 --- a/webserver.js +++ b/webserver.js @@ -5576,7 +5576,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Setup SSH if needed - if (domain.mstsc === true) { + if (domain.ssh === true) { obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); }); obj.app.ws(url + 'ssh/relay.ashx', function (ws, req) { const domain = getDomain(req);