MeshCentral can now remember RDP credentials.

This commit is contained in:
Ylian Saint-Hilaire 2021-06-29 17:13:18 -07:00
parent e373cec943
commit d4ecae73d9
8 changed files with 2351 additions and 2185 deletions

View File

@ -74,17 +74,13 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
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 = (args.tlsoffload) ? 'ws' : 'wss';
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&p=10&auth=' + obj.infos.ip; // Protocol 10 is Web-RDP
var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((obj.cookie.lc == 1) ? 'local' : 'mesh') + 'relay.ashx?noping=1&p=10&auth=' + obj.infos.ip; // Protocol 10 is Web-RDP
parent.parent.debug('relay', 'RDP: Connection websocket to ' + url);
obj.wsClient = new WebSocket(url, options);
obj.wsClient.on('open', function () { parent.parent.debug('relay', 'RDP: Relay websocket open'); });
@ -119,6 +115,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
locale: obj.infos.locale
}).on('connect', function () {
send(['rdp-connect']);
if ((typeof obj.infos.options == 'object') && (obj.infos.options.savepass == true)) { saveRdpCredentials(); } // Save the credentials if needed
}).on('bitmap', function (bitmap) {
try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
delete bitmap.data;
@ -134,13 +131,70 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
}
}
// Save SSH credentials into device
function saveRdpCredentials() {
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.rdp == null);
// Check if credentials are the same
if ((typeof node.rdp == 'object') && (node.rdp.d == obj.infos.domain) && (node.rdp.u == obj.infos.username) && (node.rdp.p == obj.infos.password)) return;
// Save the credentials
node.rdp = { d: obj.infos.domain, u: obj.infos.username, p: obj.infos.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: obj.userid, node: parent.CloneSafeNode(node), msg: "Changed RDP 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);
}
});
}
// When data is received from the web socket
// RDP default port is 3389
ws.on('message', function (msg) {
try {
msg = JSON.parse(msg);
switch (msg[0]) {
case 'infos': { obj.infos = msg[1]; startTcpServer(); break; }
case 'infos': {
obj.infos = msg[1];
// Decode the authentication cookie
obj.cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey);
if ((obj.cookie == null) || (typeof obj.cookie.nodeid != 'string') || (typeof obj.cookie.userid != 'string')) return;
obj.nodeid = obj.cookie.nodeid;
obj.userid = obj.cookie.userid;
// Check is you need to load server stored credentials
if ((typeof obj.infos.options == 'object') && (obj.infos.options.useServerCreds == true)) {
parent.parent.db.Get(obj.nodeid, function (err, nodes) {
if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
const node = nodes[0];
// Check if RDP credentials exist
if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) {
obj.infos.domain = node.rdp.d;
obj.infos.username = node.rdp.u;
obj.infos.password = node.rdp.p;
startTcpServer();
} else {
// No server credentials.
obj.infos.domain = '';
obj.infos.username = '';
obj.infos.password = '';
startTcpServer();
}
});
} else {
startTcpServer();
}
break;
}
case 'mouse': { if (rdpClient) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
case 'wheel': { if (rdpClient) { rdpClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
case 'scancode': { if (rdpClient) { rdpClient.sendKeyEventScancode(msg[1], msg[2]); } break; }
@ -417,6 +471,9 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
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;
// Save the credentials
node.ssh = { u: obj.username, p: obj.password };
parent.parent.db.Set(node);

View File

@ -713,6 +713,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Remove SSH credentials if present
if (docs[i].ssh != null) { docs[i].ssh = 1; }
// Remove RDP credentials if present
if (docs[i].rdp != null) { docs[i].rdp = 1; }
// Remove Intel AMT credential if present
if (docs[i].intelamt != null) {
if (docs[i].intelamt.pass != null) { docs[i].intelamt.pass = 1; }
@ -4275,6 +4278,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (node.ssh != null) { delete node.ssh; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
}
if ((typeof command.rdp == 'number') && (command.rdp == 0)) {
if (node.rdp != null) { delete node.rdp; change = 1; changes.push('rdp'); } // Delete the RDP cendentials
}
if (domain.geolocation && command.userloc && ((node.userloc == null) || (command.userloc[0] != node.userloc[0]) || (command.userloc[1] != node.userloc[1]))) {
change = 1;
if ((command.userloc.length == 0) && (node.userloc)) {

View File

@ -147,7 +147,7 @@
* @param password {string} session password
* @param next {function} asynchrone end callback
*/
connect : function (ip, domain, username, password, next) {
connect : function (ip, domain, username, password, options, next) {
// Start connection
var self = this;
this.socket = new WebSocket('wss://' + window.location.host + '/mstscrelay.ashx');
@ -164,6 +164,7 @@
domain: domain,
username: username,
password: password,
options: options,
locale: Mstsc.locale()
}]));
};

File diff suppressed because it is too large Load Diff

View File

@ -1876,6 +1876,7 @@
node.gpsloc = message.event.node.gpsloc;
node.tags = message.event.node.tags;
node.ssh = message.event.node.ssh;
node.rdp = message.event.node.rdp;
node.userloc = message.event.node.userloc;
node.rdpport = message.event.node.rdpport;
node.rfbport = message.event.node.rfbport;
@ -3430,13 +3431,17 @@
x += addDeviceAttribute("Tags", '<span style=color:black>' + groupingTags + '</span>');
}
// SSH Credentials
if (node.ssh != null) {
// SSH & RDP Credentials
if ((node.ssh != null) || (node.rdp != null)) {
var y = [];
if ((meshrights & 4) != 0) {
x += addDeviceAttribute("Credentials", '<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>');
if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
if (node.rdp != null) { y.push('<span onclick=showClearRdpDialog(3) style=cursor:pointer>' + "RDP" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
} else {
x += addDeviceAttribute("Credentials", "SSH");
if (node.ssh != null) { y.push("SSH"); }
if (node.rdp != null) { y.push("RDP"); }
}
x += addDeviceAttribute("Credentials", y.join(', '));
}
x += '</table><br />';
@ -3842,6 +3847,8 @@
function showClearSshDialog() { setDialogMode(2, "Edit Device", 3, showClearSshDialogEx, "Clear SSH credentials?"); }
function showClearSshDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, ssh: 0 }); }
function showClearRdpDialog() { setDialogMode(2, "Edit Device", 3, showClearRdpDialogEx, "Clear RDP credentials?"); }
function showClearRdpDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rdp: 0 }); }
var showEditNodeValueDialog_modes = ["Device Name", "Hostname", "Description", "Tags"];
var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];

View File

@ -3000,6 +3000,7 @@
node.gpsloc = message.event.node.gpsloc;
node.tags = message.event.node.tags;
node.ssh = message.event.node.ssh;
node.rdp = message.event.node.rdp;
node.userloc = message.event.node.userloc;
node.rdpport = message.event.node.rdpport;
node.rfbport = message.event.node.rfbport;
@ -6506,13 +6507,17 @@
x += addDeviceAttribute("Tags", groupingTags);
}
// SSH Credentials
if (node.ssh != null) {
// SSH & RDP Credentials
if ((node.ssh != null) || (node.rdp != null)) {
var y = [];
if ((meshrights & 4) != 0) {
x += addDeviceAttribute("Credentials", '<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>');
if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + "SSH" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
if (node.rdp != null) { y.push('<span onclick=showClearRdpDialog(3) style=cursor:pointer>' + "RDP" + ' <img class=hoverButton src="images/link5.png" width=10 height=10 /></span>'); }
} else {
x += addDeviceAttribute("Credentials", "SSH");
if (node.ssh != null) { y.push("SSH"); }
if (node.rdp != null) { y.push("RDP"); }
}
x += addDeviceAttribute("Credentials", y.join(', '));
}
x += '</table><br />';
@ -7558,6 +7563,8 @@
function showClearSshDialog() { setDialogMode(2, "Edit Device", 3, showClearSshDialogEx, "Clear SSH credentials?"); }
function showClearSshDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, ssh: 0 }); }
function showClearRdpDialog() { setDialogMode(2, "Edit Device", 3, showClearRdpDialogEx, "Clear RDP credentials?"); }
function showClearRdpDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rdp: 0 }); }
var showEditNodeValueDialog_modes = ["Device Name", "Hostname", "Description", "Tags"];
var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];

View File

@ -44,6 +44,10 @@
margin: 0 auto;
}
.formDropdown {
font-size: 17px;
}
.formLabel { }
.formControl {
@ -76,6 +80,7 @@
var urlargs = parseUriArgs();
if (urlargs.key && (isAlphaNumeric(urlargs.key) == false)) { delete urlargs.key; }
var cookie = '{{{cookie}}}';
var serverCredentials = (decodeURIComponent('{{{serverCredentials}}}') == 'true');
var name = decodeURIComponent('{{{name}}}');
if (name != '') { document.title = name + ' - ' + document.title; }
@ -90,6 +95,15 @@
QE('inputPassword', false);
QE('connectButton', false);
}
if (serverCredentials == true) {
QV('dropdowndomain', true);
Q('d3coreMode').value = 1;
} else {
QV('dropdowndomain', false);
Q('d3coreMode').value = 2;
}
dropDownChange();
}
function connect(domain, username, password) {
@ -97,11 +111,13 @@
var domain = Q('inputDomain').value;
var username = Q('inputUsername').value;
var password = Q('inputPassword').value;
var savepass = Q('inputSaveCredentials').checked;
var options = { savepass: savepass, useServerCreds: (Q('d3coreMode').value == 1) };
QV('myCanvas', true);
QV('main', false);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
client.connect(cookie, domain, username, password, function (err) { QV('myCanvas', false); QV('main', true); });
client.connect(cookie, domain, username, password, options, function (err) { QV('myCanvas', false); QV('main', true); });
return false;
}
@ -137,6 +153,16 @@
}
return r;
}
function dropDownChange() {
var newCreds = (Q('d3coreMode').value == 2);
QV('rowdomain', newCreds);
QV('rowusername', newCreds);
QV('rowpassword', newCreds);
QV('rowremember', newCreds);
if (newCreds) Q('inputUsername').focus();
}
</script>
</head>
<body onload='load()' style="position:absolute;top:0px;right:0;left:0;bottom:0px">
@ -147,18 +173,27 @@
<tr>
<td colspan="2"><hr style="color:gray;border:1px solid;" /></td>
</tr>
<tr>
<tr id="dropdowndomain" style="display:none">
<td colspan="2">
<select id=d3coreMode style=width:100%;margin-bottom:5px class="formDropdown" onchange="dropDownChange()"><option value=1>Use server credentals</option><option value=2>Use new credentals</option></select>
</td>
</tr>
<tr id="rowdomain" style="display:none">
<td><label for="inputDomain" class="formLabel">Domain</label></td>
<td style="text-align:right"><input type="text" id="inputDomain" class="formControl" placeholder="Domain"></td>
</tr>
<tr>
<tr id="rowusername" style="display:none">
<td><label for="inputUsername" class="formLabel">Username</label></td>
<td style="text-align:right"><input type="text" id="inputUsername" class="formControl" placeholder="Username"></td>
</tr>
<tr>
<tr id="rowpassword" style="display:none">
<td><label for="inputPassword" class="formLabel">Password</label></td>
<td style="text-align:right"><input type="password" id="inputPassword" class="formControl" placeholder="Password"></td>
</tr>
<tr id="rowremember" style="display:none">
<td></td>
<td><label><input type="checkbox" id="inputSaveCredentials" style="margin-left:8px;margin-right:5px">Remember credentials</label></td>
</tr>
<tr>
<td colspan="2"><button class="connectButton" onclick="return connect()">Connect</button></td>
</tr>

View File

@ -1870,7 +1870,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// This is a query with a websocket relay cookie, check that the cookie is valid and use it.
var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27') }, req, domain)); return;
// Fetch the node from the database
obj.db.Get(rcookie.nodeid, function (err, nodes) {
if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
const node = nodes[0];
// Check if we have RDP credentials for this device
var serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'));
// Render the page
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials }, req, domain));
});
return;
}
}
@ -1912,6 +1924,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
const node = nodes[0];
// Check if we have RDP credentials for this device
var serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'));
// Check access rights, must have remote control rights
if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
@ -1930,7 +1945,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Generate a cookie and respond
var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27') }, req, domain));
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials }, req, domain));
});
}
@ -7030,6 +7045,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
r = Object.assign({}, r); // Shallow clone
if (r.pmt != null) { r.pmt = 1; }
if (r.ssh != null) { r.ssh = 1; }
if (r.rdp != null) { r.rdp = 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