More work on web based SSH support.

This commit is contained in:
Ylian Saint-Hilaire 2021-04-29 15:51:06 -07:00
parent 87dc3c354d
commit abbb6be431
4 changed files with 132 additions and 124 deletions

162
ssh.js
View File

@ -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

View File

@ -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 @@
]
}
]
}
}

View File

@ -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", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=authKeyUp(event) />');
x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=authKeyUp(event) />');
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 '<table><td style=width:120px>' + t + '<td><b>' + v + '</b></table>'; }
// Parse URL arguments, only keep safe values
function parseUriArgs() {

View File

@ -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);