Fixed Windows Locking, MeshCentral certificates and more

This commit is contained in:
Ylian Saint-Hilaire 2018-07-23 17:34:24 -07:00
parent bff85f428a
commit d38cb66dda
19 changed files with 90 additions and 58 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1353,10 +1353,12 @@ function discoverMeshServerOnce() {
multicastSockets[i] = dgram.createSocket({ type: (addr.family == "IPv4" ? "udp4" : "udp6") });
multicastSockets[i].bind({ address: addr.address, exclusive: false });
if (addr.family == "IPv4") {
multicastSockets[i].addMembership(membershipIPv4);
//multicastSockets[i].setMulticastLoopback(true);
multicastSockets[i].once('message', OnMulticastMessage);
multicastSockets[i].send(settings.serverId, 16989, membershipIPv4);
try {
multicastSockets[i].addMembership(membershipIPv4);
//multicastSockets[i].setMulticastLoopback(true);
multicastSockets[i].once('message', OnMulticastMessage);
multicastSockets[i].send(settings.serverId, 16989, membershipIPv4);
} catch (e) { }
}
}
}

View File

@ -112,13 +112,9 @@ function createMeshCore(agent) {
try {
getIpLocationDataExInProgress = true;
getIpLocationDataExCounts[0]++;
http.request({
host: 'ipinfo.io', // TODO: Use a HTTP proxy if needed!!!!
port: 80,
path: 'http://ipinfo.io/json', // Use this service to get our geolocation
headers: { Host: "ipinfo.io" }
},
function (resp) {
var options = http.parseUri("http://ipinfo.io/json");
options.method = 'GET';
http.request(options, function (resp) {
if (resp.statusCode == 200) {
var geoData = '';
resp.data = function (geoipdata) { geoData += geoipdata; };
@ -755,7 +751,7 @@ function createMeshCore(agent) {
function onTunnelWebRTCControlData(data) {
if (typeof data != 'string') return;
var obj;
try { obj = JSON.parse(data); } catch (e) { sendConsoleText('Invalid control JSON on WebRTC'); return; }
try { obj = JSON.parse(data); } catch (e) { sendConsoleText('Invalid control JSON on WebRTC: ' + data); return; }
if (obj.type == 'close') {
//sendConsoleText('Tunnel #' + this.xrtc.websocket.tunnel.index + ' WebRTC control close');
try { this.close(); } catch (e) { }
@ -767,11 +763,27 @@ function createMeshCore(agent) {
function onTunnelControlData(data, ws) {
var obj;
if (ws == null) { ws = this; }
if (typeof data == 'string') { try { obj = JSON.parse(data); } catch (e) { sendConsoleText('Invalid control JSON'); return; } }
if (typeof data == 'string') { try { obj = JSON.parse(data); } catch (e) { sendConsoleText('Invalid control JSON: ' + data); return; } }
else if (typeof data == 'object') { obj = data; } else { return; }
//sendConsoleText('onTunnelControlData(' + ws.httprequest.protocol + '): ' + JSON.stringify(data));
//console.log('onTunnelControlData: ' + JSON.stringify(data));
if (obj.action) {
switch (obj.action) {
case 'lock': {
// Lock the current user out of the desktop
try {
if (process.platform == 'win32') {
var child = require('child_process');
child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 });
}
} catch (e) { }
break;
}
}
return;
}
if (obj.type == 'close') {
// We received the close on the websocket
//sendConsoleText('Tunnel #' + ws.tunnel.index + ' WebSocket control close');
@ -850,7 +862,7 @@ function createMeshCore(agent) {
var response = null;
switch (cmd) {
case 'help': { // Displays available commands
response = 'Available commands: help, info, args, print, type, dbget, dbset, dbcompact, eval, parseuri, httpget,\r\nwslist, wsconnect, wssend, wsclose, notify, ls, ps, kill, amt, netinfo, location, power, wakeonlan, scanwifi,\r\nscanamt, setdebug, smbios, rawsmbios, toast.';
response = 'Available commands: help, info, args, print, type, dbget, dbset, dbcompact, eval, parseuri, httpget,\r\nwslist, wsconnect, wssend, wsclose, notify, ls, ps, kill, amt, netinfo, location, power, wakeonlan, scanwifi,\r\nscanamt, setdebug, smbios, rawsmbios, toast, lock.';
break;
}
case 'toast': {
@ -1117,6 +1129,15 @@ function createMeshCore(agent) {
}
break;
}
case 'lsx': { // Show list of files and folders
response = objToString(getDirectoryInfo(args['_'][0]), 0, ' ', true);
break;
}
case 'lock': { // Lock the current user out of the desktop
if (process.platform == 'win32') { var child = require('child_process'); child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 }); response = 'Ok'; }
else { response = 'Not supported on the platform'; }
break;
}
case 'amt': { // Show Intel AMT status
getAmtInfo(function (state) {
var resp = 'Intel AMT not detected.';
@ -1463,7 +1484,7 @@ function createMeshCore(agent) {
if (((reqpath == undefined) || (reqpath == '')) && (process.platform == 'win32')) {
// List all the drives in the root, or the root itself
var results = null;
try { results = fs.readDrivesSync(); } catch (e) { } // TODO: Anyway to get drive total size and free space? Could draw a progress bar.
try { results = fs.readDrivesSync(); } catch (e) { sendConsoleText(e); } // TODO: Anyway to get drive total size and free space? Could draw a progress bar.
//console.log('a', objToString(results, 0, ' '));
if (results != null) {
for (var i = 0; i < results.length; ++i) {
@ -1478,7 +1499,7 @@ function createMeshCore(agent) {
var xpath = path.join(reqpath, '*');
var results = null;
try { results = fs.readdirSync(xpath); } catch (e) { }
try { results = fs.readdirSync(xpath); } catch (e) { sendConsoleText(e); }
if (results != null) {
for (var i = 0; i < results.length; ++i) {
if ((results[i] != '.') && (results[i] != '..')) {
@ -1514,6 +1535,7 @@ function createMeshCore(agent) {
return;
}
//console.log('KVM Ctrl Data', cmd);
//sendConsoleText('KVM Ctrl Data: ' + cmd);
try { cmd = JSON.parse(cmd); } catch (ex) { console.error('Invalid JSON: ' + cmd); return; }
if ((cmd.path != null) && (process.platform != 'win32') && (cmd.path[0] != '/')) { cmd.path = '/' + cmd.path; } // Add '/' to paths on non-windows
@ -1523,6 +1545,11 @@ function createMeshCore(agent) {
channel.write({ action: 'pong' });
break;
}
case 'lock': {
// Lock the current user out of the desktop
if (process.platform == 'win32') { var child = require('child_process'); child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 }); }
break;
}
case 'ls': {
/*
// Close the watcher if required

View File

@ -112,7 +112,7 @@ module.exports.CreateAmtScanner = function (parent) {
obj.performScan = function () {
//console.log('performScan');
if (obj.action == false) { return false; }
obj.parent.db.getLocalAmtNodes(10, function (err, docs) { // TODO: handler more than 10 computer scan at the same time.
obj.parent.db.getLocalAmtNodes(10, function (err, docs) { // TODO: handler more than 10 computer scan at the same time. DNS resolved may need to be a seperate module.
for (var i in obj.scanTable) { obj.scanTable[i].present = false; }
if (err == null && docs.length > 0) {
for (var i in docs) {

View File

@ -283,6 +283,9 @@ module.exports.CertificateOperations = function () {
// Fetch the name of the server
var webCertificate = obj.pki.certificateFromPem(r.web.cert);
r.CommonName = webCertificate.subject.getField('CN').value;
r.CommonNames = [ r.CommonName.toLowerCase() ];
var altNames = webCertificate.getExtension('subjectAltName')
if (altNames) { for (var i in altNames.altNames) { r.CommonNames.push(altNames.altNames[i].value.toLowerCase()); } }
var rootCertificate = obj.pki.certificateFromPem(r.root.cert);
r.RootName = rootCertificate.subject.getField('CN').value;
@ -294,14 +297,14 @@ module.exports.CertificateOperations = function () {
if (certargs == null) { commonName = r.CommonName; country = xcountry; organization = xorganization; }
// Check if we have correct certificates
if ((r.CommonName == commonName) && (xcountry == country) && (xorganization == organization) && (r.AmtMpsName == mpsCommonName)) {
if ((r.CommonNames.indexOf(commonName.toLowerCase()) >= 0) && (r.AmtMpsName == mpsCommonName)) {
// Certificate matches what we want, keep it.
if (func != undefined) { func(r); } return r;
} else {
// Check what certificates we really need to re-generate.
if ((r.CommonName != commonName) || (xcountry != country) || (xorganization != organization)) { forceWebCertGen = 1; }
if ((r.CommonNames.indexOf(commonName.toLowerCase()) < 0)) { forceWebCertGen = 1; }
if (r.AmtMpsName != mpsCommonName) { forceMpsCertGen = 1; }
}
}
}
console.log('Generating certificates, may take a few minutes...');
parent.updateServerState('state', 'generatingcertificates');

View File

@ -77,7 +77,7 @@ function CreateMeshCentralServer(config, args) {
// Start the Meshcentral server
obj.Start = function () {
try { require('./pass').hash('test', function () { }); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
// Check for invalid arguments
var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpsdebug', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbimport', 'selfupdate', 'tlsoffload', 'userallowedip', 'fastcert', 'swarmport', 'swarmdebug', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin'];
for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }

View File

@ -61,13 +61,11 @@ module.exports.CreateMeshScanner = function (parent) {
if (server4.xxlocal != '*') { bindOptions.address = server4.xxlocal; }
server4.bind(bindOptions, function () {
try {
this.setBroadcast(true);
this.setMulticastTTL(128);
this.addMembership(membershipIPv4);
var doscan = true;
try { this.setBroadcast(true); this.setMulticastTTL(128); this.addMembership(membershipIPv4); } catch (e) { doscan = false; }
this.on('error', function (error) { console.log('Error: ' + error); });
this.on('message', function (msg, info) { onUdpPacket(msg, info, this); });
obj.performScan(this);
obj.performScan(this);
if (doscan == true) { obj.performScan(this); obj.performScan(this); }
} catch (e) { console.log(e); }
});
obj.servers4[localAddress] = server4;
@ -94,13 +92,11 @@ module.exports.CreateMeshScanner = function (parent) {
if (server6.xxlocal != '*') { bindOptions.address = server6.xxlocal; }
server6.bind(bindOptions, function () {
try {
this.setBroadcast(true);
this.setMulticastTTL(128);
this.addMembership(membershipIPv6);
var doscan = true;
try { this.setBroadcast(true); this.setMulticastTTL(128); this.addMembership(membershipIPv6); } catch (e) { doscan = false; }
this.on('error', function (error) { console.log('Error: ' + error); });
this.on('message', function (msg, info) { onUdpPacket(msg, info, this); });
obj.performScan(this);
obj.performScan(this);
if (doscan == true) { obj.performScan(this); obj.performScan(this); }
} catch (e) { console.log(e); }
});
obj.servers6[localAddress] = server6;

View File

@ -414,7 +414,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
var WindowSize = common.ReadInt(data, 9);
socket.tag.activetunnels++;
var cirachannel = socket.tag.channels[RecipientChannel];
if (cirachannel == undefined) { /*console.log("MPS Error in CHANNEL_OPEN_CONFIRMATION: Unable to find channelid " + RecipientChannel);*/ return; }
if (cirachannel == undefined) { /*console.log("MPS Error in CHANNEL_OPEN_CONFIRMATION: Unable to find channelid " + RecipientChannel);*/ return 17; }
cirachannel.amtchannelid = SenderChannel;
cirachannel.sendcredits = cirachannel.amtCiraWindow = WindowSize;
Debug(3, 'MPS:CHANNEL_OPEN_CONFIRMATION', RecipientChannel, SenderChannel, WindowSize);
@ -450,7 +450,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
var ReasonCode = common.ReadInt(data, 5);
Debug(3, 'MPS:CHANNEL_OPEN_FAILURE', RecipientChannel, ReasonCode);
var cirachannel = socket.tag.channels[RecipientChannel];
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_OPEN_FAILURE: Unable to find channelid " + RecipientChannel); return; }
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_OPEN_FAILURE: Unable to find channelid " + RecipientChannel); return 17; }
if (cirachannel.state > 0) {
cirachannel.state = 0;
if (cirachannel.onStateChange) { cirachannel.onStateChange(cirachannel, cirachannel.state); }
@ -464,7 +464,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
var RecipientChannel = common.ReadInt(data, 1);
Debug(3, 'MPS:CHANNEL_CLOSE', RecipientChannel);
var cirachannel = socket.tag.channels[RecipientChannel];
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_CLOSE: Unable to find channelid " + RecipientChannel); return; }
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_CLOSE: Unable to find channelid " + RecipientChannel); return 5; }
socket.tag.activetunnels--;
if (cirachannel.state > 0) {
cirachannel.state = 0;
@ -479,7 +479,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
var RecipientChannel = common.ReadInt(data, 1);
var ByteToAdd = common.ReadInt(data, 5);
var cirachannel = socket.tag.channels[RecipientChannel];
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_WINDOW_ADJUST: Unable to find channelid " + RecipientChannel); return; }
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_WINDOW_ADJUST: Unable to find channelid " + RecipientChannel); return 9; }
cirachannel.sendcredits += ByteToAdd;
Debug(3, 'MPS:CHANNEL_WINDOW_ADJUST', RecipientChannel, ByteToAdd, cirachannel.sendcredits);
if (cirachannel.state == 2 && cirachannel.sendBuffer != undefined) {
@ -507,7 +507,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
if (len < (9 + LengthOfData)) return 0;
Debug(4, 'MPS:CHANNEL_DATA', RecipientChannel, LengthOfData);
var cirachannel = socket.tag.channels[RecipientChannel];
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_DATA: Unable to find channelid " + RecipientChannel); return; }
if (cirachannel == undefined) { console.log("MPS Error in CHANNEL_DATA: Unable to find channelid " + RecipientChannel); return 9 + LengthOfData; }
cirachannel.amtpendingcredits += LengthOfData;
if (cirachannel.onData) cirachannel.onData(cirachannel, data.substring(9, 9 + LengthOfData));
if (cirachannel.amtpendingcredits > (cirachannel.ciraWindow / 2)) {

View File

@ -1,6 +1,6 @@
{
"name": "meshcentral",
"version": "0.1.8-r",
"version": "0.1.8-y",
"keywords": [
"Remote Management",
"Intel AMT",

View File

@ -272,7 +272,7 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
}
obj.SendKeyMsgKC = function (action, kc) {
console.log('SendKeyMsgKC', action, kc);
//console.log('SendKeyMsgKC', action, kc);
if (obj.State != 3) return;
if (typeof action == 'object') { for (var i in action) { obj.SendKeyMsgKC(action[i][0], action[i][1]); } }
else { obj.send(String.fromCharCode(0x00, obj.InputType.KEY, 0x00, 0x06, (action - 1), kc)); }

View File

@ -63,26 +63,25 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
obj.webSwitchOk = true; // Other side is ready for switch over
performWebRtcSwitch();
} else if (controlMsg.type == 'webrtc1') {
sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc2\"}"); // Confirm we got end of data marker, indicates data will no longer be received on websocket.
obj.sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc2\"}"); // Confirm we got end of data marker, indicates data will no longer be received on websocket.
} else if (controlMsg.type == 'webrtc2') {
// TODO: Resume/Start sending data over WebRTC
}
}
}
function sendCtrlMsg(x) { if (obj.ctrlMsgAllowed == true) { try { obj.socket.send(x); } catch (ex) { } } }
obj.sendCtrlMsg = function (x) { if (obj.ctrlMsgAllowed == true) { if (args && args.redirtrace) { console.log('RedirSend', typeof x, x); } try { obj.socket.send(x); } catch (ex) { } } }
function performWebRtcSwitch() {
if ((obj.webSwitchOk == true) && (obj.webRtcActive == true)) {
sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}"); // Indicate to the meshagent that it can start traffic switchover
sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc1\"}"); // Indicate to the meshagent that data traffic will no longer be sent over websocket.
obj.sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}"); // Indicate to the meshagent that it can start traffic switchover
obj.sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc1\"}"); // Indicate to the meshagent that data traffic will no longer be sent over websocket.
// TODO: Hold/Stop sending data over websocket
if (obj.onStateChanged != null) { obj.onStateChanged(obj, obj.State); }
}
}
obj.xxOnMessage = function (e) {
//if (obj.debugmode == 1) { console.log('Recv', e.data); }
//console.log('Recv', e.data, obj.State);
if (obj.State < 3) {
if (e.data == 'c') {
@ -149,7 +148,6 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
}
} else {
// If we get a string object, it maybe the WebRTC confirm. Ignore it.
//obj.debug("Agent Redir Relay - OnData - " + typeof e.data + " - " + e.data.length);
obj.xxOnSocketData(e.data);
}
};
@ -164,6 +162,7 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
}
else if (typeof data !== 'string') return;
//console.log("xxOnSocketData", rstr2hex(data));
if (args && args.redirtrace) { console.log("RedirRecv", typeof data, data.length, data); }
return obj.m.ProcessData(data);
}
@ -175,6 +174,7 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
obj.send = function (x) {
//obj.debug("Agent Redir Send(" + obj.webRtcActive + ", " + x.length + "): " + rstr2hex(x));
//console.log("Agent Redir Send(" + obj.webRtcActive + ", " + x.length + "): " + ((typeof x == 'string')?x:rstr2hex(x)));
if (args && args.redirtrace) { console.log('RedirSend', typeof x, x.length, x); }
try {
if (obj.socket != null && obj.socket.readyState == WebSocket.OPEN) {
if (typeof x == 'string') {
@ -225,7 +225,7 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
//obj.debug("Agent Redir Socket Stopped");
obj.connectstate = -1;
if (obj.socket != null) {
try { if (obj.socket.readyState == 1) { sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"close\"}"); obj.socket.close(); } } catch (e) { }
try { if (obj.socket.readyState == 1) { obj.sendCtrlMsg("{\"ctrlChannel\":\"102938\",\"type\":\"close\"}"); obj.socket.close(); } } catch (e) { }
obj.socket = null;
}
obj.xxStateChange(0);

View File

@ -3,7 +3,7 @@ MeshCentral
For more information, [visit MeshCommander.com/MeshCentral2](http://www.meshcommander.com/meshcentral2).
Download the [full PDF user's guide](http://info.meshcentral.com/downloads/meshcentral2/MeshCentral2UserGuide.pdf) with more information on installing, configuring and running MeshCentral2.
Download the [full PDF user's guide](http://info.meshcentral.com/downloads/meshcentral2/MeshCentral2UserGuide.pdf) with more information on configuring and running MeshCentral2. In addition, the [installation guide](http://info.meshcentral.com/downloads/meshcentral2/MeshCentral2InstallGuide.pdf) can help get MeshCentral installed on Amazon AWS, Microsoft Azure, Ubuntu and the Raspberry Pi.
This is a full computer management web site. With MeshCentral, you can run your own web server to remotely manage and control computers on a local network or anywhere on the internet. Once you get the server started, create a mesh (a group of computers) and then download and install a mesh agent on each computer you want to manage. A minute later, the new computer will show up on the web site and you can take control of it. MeshCentral includes full web-based remote desktop, terminal and file management capability.

View File

@ -1,15 +1,17 @@
{
"__comment__" : "This is a sample configuration file, edit a section and remove the _ in front of the name. Refer to the user's guide for details.",
"_settings": {
"MongoDb": "mongodb://127.0.0.1:27017/meshcentral",
"MongoDbCol": "meshcentral",
"Port": 443,
"RedirPort": 80,
"AllowLoginToken": true,
"AllowFraming": true,
"WebRTC": false,
"ClickOnce": false,
"UserAllowedIP" : "127.0.0.1,::1,192.168.0.100"
"settings": {
"_MongoDb": "mongodb://127.0.0.1:27017/meshcentral",
"_MongoDbCol": "meshcentral",
"_WANonly": true,
"_LANonly": true,
"_Port": 443,
"_RedirPort": 80,
"_AllowLoginToken": true,
"_AllowFraming": true,
"_WebRTC": false,
"_ClickOnce": false,
"_UserAllowedIP" : "127.0.0.1,::1,192.168.0.100"
},
"_domains": {
"": {

View File

@ -3622,11 +3622,12 @@
if (desktop.contype == 2) {
desktop.m.sendkey([[0xffe7,1],[0x6c,1],[0x6c,0],[0xffe7,0]]); // Intel AMT: Meta-left down, 'l' press, 'l' release, Meta-left release
} else {
desktop.sendCtrlMsg('{"action":"lock"}');
//desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN,0x5B],[desktop.m.KeyAction.DOWN,76],[desktop.m.KeyAction.UP,76],[desktop.m.KeyAction.EXUP,0x5B]]); // MeshAgent: L-Winkey press, 'L' press, 'L' release, L-Winkey release
desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXDOWN, 0x5B);
//desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXDOWN, 0x5B);
//desktop.m.SendKeyMsgKC(desktop.m.KeyAction.DOWN, 76);
//desktop.m.SendKeyMsgKC(desktop.m.KeyAction.UP, 76);
desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXUP, 0x5B);
//desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXUP, 0x5B);
}
} else if (ks == 3) { // WIN+M arrow
if (desktop.contype == 2) {
@ -5190,6 +5191,7 @@
x += '</table>';
if (hiddenUsers == 1) { x += '<br />1 more user not shown, use search box to look for users...<br />'; }
else if (hiddenUsers > 1) { x += '<br />' + hiddenUsers + ' more users not shown, use search box to look for users...<br />'; }
if (maxUsers == 100) { x += '<br />No users found.<br />'; }
QH('p3users', x);
// Update current user panel if needed