Added new allowSavingDeviceCredentials option, #3751

Ylian Saint-Hilaire committed Mar 15, 2022 at 17:00 UTC 545290a9af92b924b722464b88249f2d11aae8db
6 files changed +82 -39
apprelays.js
+8 -4
@@ -168,6 +168,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
168
169 // Save SSH credentials into device
170 function saveRdpCredentials() {
171 + if (domain.allowsavingdevicecredentials == false) return;
172 parent.parent.db.Get(obj.nodeid, function (err, nodes) {
173 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
174 const node = nodes[0];
@@ -214,7 +215,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
215 // Check if we need to load server stored credentials
216 if ((typeof obj.infos.options == 'object') && (obj.infos.options.useServerCreds == true)) {
217 // Check if RDP credentials exist
217 - if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) {
218 + if ((domain.allowsavingdevicecredentials === false) && (typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) {
219 obj.infos.domain = node.rdp.d;
220 obj.infos.username = node.rdp.u;
221 obj.infos.password = node.rdp.p;
@@ -340,6 +341,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
341
342 // Save SSH credentials into device
343 function saveSshCredentials() {
344 + if (domain.allowsavingdevicecredentials == false) return;
345 parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) {
346 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
347 const node = nodes[0];
@@ -471,7 +473,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
473 parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) {
474 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
475 const node = nodes[0];
474 - if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
476 + if ((domain.allowsavingdevicecredentials === false) || (node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
477 // Send a request for SSH authentication
478 try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
479 } else {
@@ -611,6 +613,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
613
614 // Save SSH credentials into device
615 function saveSshCredentials() {
616 + if (domain.allowsavingdevicecredentials == false) return;
617 parent.parent.db.Get(obj.nodeid, function (err, nodes) {
618 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
619 const node = nodes[0];
@@ -811,7 +814,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
814 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
815 const node = nodes[0];
816
814 - if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
817 + if ((domain.allowsavingdevicecredentials === false) || (node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
818 // Send a request for SSH authentication
819 try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
820 } else {
@@ -903,6 +906,7 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
906
907 // Save SSH credentials into device
908 function saveSshCredentials() {
909 + if (domain.allowsavingdevicecredentials == false) return;
910 parent.parent.db.Get(obj.nodeid, function (err, nodes) {
911 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
912 const node = nodes[0];
@@ -1283,7 +1287,7 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
1287 if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
1288 const node = nodes[0];
1289
1286 - if ((node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
1290 + if ((domain.allowsavingdevicecredentials === false) || (node.ssh == null) || (typeof node.ssh != 'object') || (typeof node.ssh.u != 'string') || ((typeof node.ssh.p != 'string') && (typeof node.ssh.k != 'string'))) {
1291 // Send a request for SSH authentication
1292 try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { }
1293 } else {
meshcentral-config-schema.json
+1
@@ -321,6 +321,7 @@
321 "hide": { "type": "integer", "default": 0, "description": "Sum of: 1 = Hide header, 2 = Hide tab, 4 = Hide footer, 8 = Hide title, 16 = Hide left bar, 32 = Hide back buttons" },
322 "footer": { "type": "string", "default": null, "description": "This is a HTML string displayed at the bottom of the web page when a user is logged in." },
323 "loginfooter": { "type": "string", "default": null, "description": "This is a HTML string displayed at the bottom of the web page when a user is not logged in." },
324 + "allowSavingDeviceCredentials": { "type": "boolean", "default": true, "description": "Allow users to save SSH, RDP, VNC device credentials on the server that can be used by any other user." },
325 "guestDeviceSharing": {
326 "type": [ "boolean", "object" ],
327 "default": true,
views/default.handlebars
+38 -24
@@ -2434,14 +2434,14 @@
2434 if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == message.nodeid) { index = i; break; } } }
2435 if (index != -1) {
2436 // Node was found, dispatch the message
2437 - if ((message.type == 'cpuinfo') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2437 + if ((message.type === 'cpuinfo') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2438 var now = (Date.now() / 1000), cpu = 0, memory = 0;
2439 if (typeof message.cpu.total == 'number') { cpu = message.cpu.total; }
2440 if (typeof message.memory.percentConsumed == 'number') { memory = message.memory.percentConsumed; }
2441 deviceDetailsStatsData.push([now, cpu, memory]);
2442 deviceDetailsStatsDraw(message);
2443 - } else if (message.type == 'console') { p15consoleReceive(nodes[index], message.value, message.source); } // This is a console message.
2444 - else if (message.type == 'notify') { // This is a notification message.
2443 + } else if (message.type === 'console') { p15consoleReceive(nodes[index], message.value, message.source); } // This is a console message.
2444 + else if (message.type === 'notify') { // This is a notification message.
2445 var n = getstore('notifications', 0);
2446 if (((n & 8) == 0) && (message.amtMessage != null)) { break; } // Intel AMT desktop & terminal messages should be ignored.
2447 var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
@@ -2450,40 +2450,40 @@
2450 if (message.tag != null) { n.tag = message.tag; }
2451 if (message.url != null) { n.url = message.url; }
2452 if (message.username != null) { n.username = message.username; }
2453 - if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
2453 + if (typeof message.maxtime === 'number') { n.maxtime = message.maxtime; }
2454 addNotification(n);
2455 - } else if (message.type == 'ps') {
2455 + } else if (message.type === 'ps') {
2456 showDeskToolsProcesses(message);
2457 - } else if (message.type == 'services') {
2457 + } else if (message.type === 'services') {
2458 showDeskToolsServices(message);
2459 - } else if ((message.type == 'getclip') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2460 - if ((message.tag == 1) && (xxdialogTag == 'clipboard')) {
2459 + } else if ((message.type === 'getclip') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2460 + if ((message.tag == 1) && (xxdialogTag === 'clipboard')) {
2461 Q('d2clipText').value = message.data; // Put remote clipboard data into dialog box
2462 - } else if (message.tag == 2) {
2462 + } else if (message.tag === 2) {
2463 if (navigator.clipboard != null) { navigator.clipboard.writeText(message.data).then(function() { }).catch(function(err) { console.log(err); }) } // Put remote clipboard data into our clipboard
2464 }
2465 - } else if ((message.type == 'setclip') && (xxdialogTag == 'clipboard') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2465 + } else if ((message.type === 'setclip') && (xxdialogTag === 'clipboard') && (currentNode != null) && (currentNode._id == message.nodeid)) {
2466 // Display success/fail on the clipboard dialog box.
2467 QH('dlgClipStatus', message.success ? '<span style=color:green>' + "Success" + '</span>' : '<span style=color:red>' + "Failed" + '</span>')
2468 setTimeout(function () { try { QH('dlgClipStatus', ''); } catch (ex) { } }, 2000);
2469 - } else if ((message.type == 'userSessions') && (currentNode != null) && (currentNode._id == message.nodeid) && (desktop == null)) {
2469 + } else if ((message.type === 'userSessions') && (currentNode != null) && (currentNode._id === message.nodeid) && (desktop == null)) {
2470 // Got list of user sessions
2471 var userSessions = [];
2472 - if (message.data != null) { for (var i in message.data) { if ((message.data[i].State == 'Active') || (message.data[i].StationName == 'Console') || (debugmode == 3)) { userSessions.push(message.data[i]); } } }
2473 - if (userSessions.length == 0) { connectDesktop(null, 1, null, message.tag); } // No active sessions, do a normal connection.
2474 - else if (userSessions.length == 1) { connectDesktop(null, 1, userSessions[0].SessionId, message.tag); } // One active session, connect to it
2472 + if (message.data != null) { for (var i in message.data) { if ((message.data[i].State == 'Active') || (message.data[i].State == 'Connected') || (message.data[i].StationName == 'Console') || (debugmode == 3)) { userSessions.push(message.data[i]); } } }
2473 + if (userSessions.length === 0) { connectDesktop(null, 1, null, message.tag); } // No active sessions, do a normal connection.
2474 + else if (userSessions.length === 1) { connectDesktop(null, 1, userSessions[0].SessionId, message.tag); } // One active session, connect to it
2475 else {
2476 var x = '';
2477 for (var i in userSessions) {
2478 - x += '<div style="text-align:left;cursor:pointer;background-color:gray;margin:5px;padding:5px;border-radius:5px" onclick=connectDesktop(event,1,' + userSessions[i].SessionId + ',' + message.tag + ')>' + userSessions[i].State + ', ' + userSessions[i].StationName;
2478 + x += '<div style="text-align:left;cursor:pointer;background-color:gray;margin:5px;padding:5px;border-radius:5px" onclick=connectDesktop(event,1,' + userSessions[i].SessionId + ',' + message.tag + ')>' + userSessions[i].State + (userSessions[i].StationName ? (', ' + userSessions[i].StationName) : '');
2479 if (userSessions[i].Username) { if (userSessions[i].Domain) { x += ' - ' + userSessions[i].Domain + '/' + userSessions[i].Username; } else { x += ' - ' + userSessions[i].Username; } }
2480 x += '</div>';
2481 }
2482 QH('p11DeskSessionSelector', x);
2483 QV('p11DeskSessionSelector', true);
2484 }
2485 - } else if (message.type == 'psinfo') {
2486 - if (xxdialogTag == ('ps|' + message.nodeid + '|' + message.pid)) {
2485 + } else if (message.type === 'psinfo') {
2486 + if (xxdialogTag === ('ps|' + message.nodeid + '|' + message.pid)) {
2487 var x = '<div style=max-height:200px;overflow-y:auto>';
2488 //x += addHtmlValue4("Process ID", message.pid);
2489 if ((typeof message.value == 'object') && (Object.keys(message.value).length > 0)) {
@@ -2518,7 +2518,7 @@
2518 }
2519 }
2520 } else {
2521 - if (message.type == 'notify') { // This is a notification message.
2521 + if (message.type === 'notify') { // This is a notification message.
2522 var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
2523 if (message.id != null) { n.id = message.id; }
2524 if (message.tag != null) { n.tag = message.tag; }
@@ -9489,8 +9489,9 @@
9489 x += addHtmlValue("Key File", '<input type=file id=dp2key style=width:230px maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "Key file must be in OpenSSH format." + '</div>');
9490 x += addHtmlValue("Key Password", '<input type=password id=dp2keypass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
9491 x += '</div>';
9492 - x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
9492 + if ((features2 & 0x00400000) == 0) { x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>'); }
9493 setDialogMode(2, "Authentication", 11, sshConnectEx, x, 'ssh');
9494 + Q('dp2user').focus();
9495 setTimeout(sshAuthUpdate, 50);
9496 break;
9497 }
@@ -9525,15 +9526,25 @@
9526 reader.readAsText(Q('dp2key').files[0]);
9527 }
9528 }
9529 +
9530 + // When the enter key is pressed, move to the next field
9531 + if (e && (e.keyCode == 13) && (e.target) && (Q('dp2authmethod').value == 1)) {
9532 + if (e.target.id == 'dp2user') { Q('dp2pass').focus(); }
9533 + if (e.target.id == 'dp2pass') { dialogclose(1); }
9534 + }
9535 }
9536 +
9537 function sshConnectEx(b) {
9538 if (b == 0) {
9539 if (terminal != null) { connectTerminal(); } // Disconnect
9540 } else {
9541 + var keep = false;
9542 + if ((features2 & 0x00400000) == 0) { keep = Q('dp2keep').checked; }
9543 +
9544 if (Q('dp2authmethod').value == 1) {
9534 - 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 }));
9545 + terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: keep, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
9546 } else {
9536 - var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value, keep = Q('dp2keep').checked;
9547 + var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value;
9548 reader.onload = function (e) { terminal.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight })); }
9549 reader.readAsText(Q('dp2key').files[0]);
9550 }
@@ -9881,8 +9892,9 @@
9892 x += addHtmlValue("Key File", '<input type=file id=dp2key style=width:230px maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />');
9893 x += addHtmlValue("Key Password", '<input type=password id=dp2keypass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
9894 x += '</div>';
9884 - x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
9895 + if ((features2 & 0x00400000) == 0) { x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>'); }
9896 setDialogMode(2, "Authentication", 11, p13sshConnectEx, x, 'ssh');
9897 + Q('dp2user').focus();
9898 setTimeout(sshAuthUpdate, 50);
9899 return;
9900 }
@@ -9938,10 +9950,12 @@
9950 if (b == 0) {
9951 if (files != null) { connectFiles(); } // Disconnect
9952 } else {
9953 + var keep = false;
9954 + if ((features2 & 0x00400000) == 0) { keep = Q('dp2keep').checked; }
9955 if (Q('dp2authmethod').value == 1) {
9942 - files.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: Q('dp2keep').checked }));
9956 + files.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: keep }));
9957 } else {
9944 - var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value, keep = Q('dp2keep').checked;
9958 + var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value;
9959 reader.onload = function (e) { files.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep })); }
9960 reader.readAsText(Q('dp2key').files[0]);
9961 }
views/mstsc.handlebars
+2 -1
@@ -83,6 +83,7 @@
83 var serverCredentials = (decodeURIComponent('{{{serverCredentials}}}') == 'true');
84 var name = decodeURIComponent('{{{name}}}');
85 if (name != '') { document.title = name + ' - ' + document.title; }
86 + var features = parseInt('{{{features}}}');
87
88 function load() {
89 if (name != '') { QH('computerName', EscapeHtml(name)); }
@@ -159,7 +160,7 @@
160 QV('rowdomain', newCreds);
161 QV('rowusername', newCreds);
162 QV('rowpassword', newCreds);
162 - QV('rowremember', newCreds);
163 + QV('rowremember', newCreds && ((features & 1) == 0));
164 if (newCreds) Q('inputUsername').focus();
165 }
166
views/ssh.handlebars
+10 -2
@@ -75,6 +75,7 @@
75 if (urlargs.key && (isAlphaNumeric(urlargs.key) == false)) { delete urlargs.key; }
76 var cookie = '{{{cookie}}}';
77 var domainurl = '{{{domainurl}}}';
78 + var features = parseInt('{{{features}}}');
79 var name = decodeURIComponent('{{{name}}}');
80 if (name != '') { document.title = name + ' - ' + document.title; }
81 var StatusStrs = ["Disconnected", "Connecting...", "Setup...", "Connected"];
@@ -146,10 +147,17 @@
147 reader.readAsText(Q('dp2key').files[0]);
148 }
149 }
150 +
151 + // When the enter key is pressed, move to the next field
152 + if (e && (e.keyCode == 13) && (e.target) && (Q('dp2authmethod').value == 1)) {
153 + if (e.target.id == 'dp2user') { Q('dp2pass').focus(); }
154 + if (e.target.id == 'dp2pass') { dialogclose(1); }
155 + }
156 }
157
158 function connectEx() {
152 - 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 };
159 + var cmd = { action: 'connect', cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight, username: Q('dp2user').value, keep: false };
160 + if ((features & 1) == 0) { cmd.keep = Q('dp2keep').checked; }
161
162 if (Q('dp2authmethod').value == 1) {
163 cmd.password = Q('dp2pass').value;
@@ -191,7 +199,7 @@
199 x += addHtmlValue("Key File", '<input type=file id=dp2key style=width:230px maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "Key file must be in OpenSSH format." + '</div>');
200 x += addHtmlValue("Key Password", '<input type=password id=dp2keypass style=width:230px maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
201 x += '</div>';
194 - x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>');
202 + if ((features & 1) == 0) { x += addHtmlValue('', '<label><input id=dp2keep type=checkbox>' + "Remember credentials" + '</label>'); }
203 setDialogMode(2, "Authentication", 3, connectEx, x);
204 Q('dp2user').value = user;
205 Q('dp2pass').value = pass;
webserver.js
+23 -8
@@ -1949,6 +1949,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1949 return;
1950 }
1951
1952 + // Set features we want to send to this page
1953 + var features = 0;
1954 + if (domain.allowsavingdevicecredentials === false) { features |= 1; }
1955 +
1956 if (req.query.ws != null) {
1957 // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
1958 var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
@@ -1960,10 +1964,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1964 const node = nodes[0];
1965
1966 // Check if we have RDP credentials for this device
1963 - var serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'));
1967 + var serverCredentials = false;
1968 + if (domain.allowsavingdevicecredentials !== false) {
1969 + if (page == 'ssh') {
1970 + serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'))
1971 + } else {
1972 + serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string'))
1973 + }
1974 + }
1975
1976 // Render the page
1966 - render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials }, req, domain));
1977 + render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
1978 });
1979 return;
1980 }
@@ -2000,35 +2011,38 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2011 }
2012
2013 // If there is no nodeid, exit now
2003 - if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '' }, req, domain)); return; }
2014 + if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
2015
2016 // Fetch the node from the database
2017 obj.db.Get(req.query.node, function (err, nodes) {
2018 if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2019 const node = nodes[0];
2020
2010 - // Check if we have RDP credentials for this device
2011 - var serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'));
2012 -
2021 // Check access rights, must have remote control rights
2022 if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
2023
2024 // Figure out the target port
2017 - var port = 0;
2025 + var port = 0, serverCredentials = false;
2026 if (page == 'ssh') {
2027 // SSH port
2028 port = 22;
2029 if (typeof node.sshport == 'number') { port = node.sshport; }
2030 +
2031 + // Check if we have SSH credentials for this device
2032 + if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string')); }
2033 } else {
2034 // RDP port
2035 port = 3389;
2036 if (typeof node.rdpport == 'number') { port = node.rdpport; }
2037 +
2038 + // Check if we have RDP credentials for this device
2039 + if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')); }
2040 }
2041 if (req.query.port != null) { var qport = 0; try { qport = parseInt(req.query.port); } catch (ex) { } if ((typeof qport == 'number') && (qport > 0) && (qport < 65536)) { port = qport; } }
2042
2043 // Generate a cookie and respond
2044 var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
2031 - render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials }, req, domain));
2045 + render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
2046 });
2047 }
2048
@@ -2942,6 +2956,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2956 if ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
2957 if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
2958 if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
2959 + if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
2960 return { features: features, features2: features2 };
2961 }
2962