mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2025-03-09 15:40:18 +00:00
Compare commits
231 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a765bb13 | ||
|
|
7ad4b917be | ||
|
|
d10173a018 | ||
|
|
0e3a6b4915 | ||
|
|
b949cecc5f | ||
|
|
c7cbf2f12a | ||
|
|
d49afdd7bf | ||
|
|
133e77c8c6 | ||
|
|
e404e86b9f | ||
|
|
c6da201af8 | ||
|
|
9a27d7637c | ||
|
|
5aa2467409 | ||
|
|
9398afd07e | ||
|
|
b2cd84035b | ||
|
|
97547d72a3 | ||
|
|
7faf043c35 | ||
|
|
9df0330896 | ||
|
|
42f61ea46e | ||
|
|
0d65080a8a | ||
|
|
bd4d8b12d4 | ||
|
|
18ae8bdbf4 | ||
|
|
46c76f7234 | ||
|
|
e65bf77111 | ||
|
|
f19ad6c664 | ||
|
|
fe2f12149d | ||
|
|
91bd5ae702 | ||
|
|
38f5bf2e0f | ||
|
|
79f00bcaab | ||
|
|
c55065505b | ||
|
|
e0e8a3fcaa | ||
|
|
3f77cfa93a | ||
|
|
5ee9aa2410 | ||
|
|
0ab3f01ca6 | ||
|
|
4b621a01fb | ||
|
|
f2681de87d | ||
|
|
c90fa55c99 | ||
|
|
edeef03f00 | ||
|
|
d7fe87d1db | ||
|
|
9d4f51e970 | ||
|
|
0b376fe5a0 | ||
|
|
9fd40751b2 | ||
|
|
d246307fae | ||
|
|
711bb56a93 | ||
|
|
5734bcc33a | ||
|
|
1310c57397 | ||
|
|
854d6c00d2 | ||
|
|
c96d7ff1ca | ||
|
|
712f06db3c | ||
|
|
cac505e2cd | ||
|
|
9d962bc523 | ||
|
|
f079692b16 | ||
|
|
3ee06abfe8 | ||
|
|
b46ddf2f70 | ||
|
|
f7b958d28b | ||
|
|
64c8d2c238 | ||
|
|
31f2224a93 | ||
|
|
de685556c8 | ||
|
|
92375ddc93 | ||
|
|
ea80f8595e | ||
|
|
763f76b68f | ||
|
|
1a02539f23 | ||
|
|
90b71e924f | ||
|
|
73c18c4dd5 | ||
|
|
def62075c7 | ||
|
|
998769a888 | ||
|
|
ca6ec5ebc9 | ||
|
|
0dd56d5708 | ||
|
|
95729d2a88 | ||
|
|
1ae7b9f641 | ||
|
|
c66a9a12ef | ||
|
|
2c310450cf | ||
|
|
b6e022c01e | ||
|
|
e66776c5da | ||
|
|
d1cb184e9e | ||
|
|
6aa60d556f | ||
|
|
8d4e9bcede | ||
|
|
eb0d24cf0b | ||
|
|
fe02d3158d | ||
|
|
61d3487f8a | ||
|
|
a23725eb40 | ||
|
|
9b60271f31 | ||
|
|
31f328086e | ||
|
|
7aa4061cad | ||
|
|
54bb0177ee | ||
|
|
ce70f4ac08 | ||
|
|
f712cd9425 | ||
|
|
c4592dcc4f | ||
|
|
2a274fe569 | ||
|
|
5d0b5acdfb | ||
|
|
f80ba62cfc | ||
|
|
5da849063b | ||
|
|
68ac8cf86c | ||
|
|
8e70cd7187 | ||
|
|
5cf468159d | ||
|
|
c92b88a374 | ||
|
|
1b01b90cd6 | ||
|
|
6a366fe174 | ||
|
|
e2362a0547 | ||
|
|
59fcc0dbc6 | ||
|
|
988983b880 | ||
|
|
a1854fa074 | ||
|
|
22fc95926a | ||
|
|
c2eb1f2516 | ||
|
|
832d11739b | ||
|
|
ab7be919a0 | ||
|
|
624d61db43 | ||
|
|
b0d8e3fe48 | ||
|
|
a33047747a | ||
|
|
39e81befe1 | ||
|
|
8eeb96fb0d | ||
|
|
f9228ad0eb | ||
|
|
ce4217c346 | ||
|
|
d9262f7c9d | ||
|
|
8e8ec4f88a | ||
|
|
18167499d9 | ||
|
|
da5d03b0e7 | ||
|
|
c41eb72a2c | ||
|
|
ef4d764ab4 | ||
|
|
c16ff89902 | ||
|
|
041e8021d1 | ||
|
|
6e31562ecb | ||
|
|
874ef23c40 | ||
|
|
7bd5b66ebc | ||
|
|
975e49a190 | ||
|
|
462c383b77 | ||
|
|
dbb5b4ba11 | ||
|
|
545bf58e8d | ||
|
|
30b390bdbf | ||
|
|
d0a51e90e9 | ||
|
|
c773857b17 | ||
|
|
cae1f7ea14 | ||
|
|
b398cb7fa9 | ||
|
|
5a1a97ca7e | ||
|
|
dd21f14f4e | ||
|
|
3da60b43ac | ||
|
|
727080ab68 | ||
|
|
54170c44a0 | ||
|
|
8a5ad1563d | ||
|
|
911d987a84 | ||
|
|
9a8f4e8ebe | ||
|
|
badee98b71 | ||
|
|
d44faed29e | ||
|
|
01c585f7f1 | ||
|
|
777eb53476 | ||
|
|
b71c69e81d | ||
|
|
7d59210d05 | ||
|
|
fc387ca417 | ||
|
|
fc83211e90 | ||
|
|
9ebd23a518 | ||
|
|
3f8301e9d7 | ||
|
|
b39235643e | ||
|
|
0ec8b061c8 | ||
|
|
e58d659fa9 | ||
|
|
45169b2cfd | ||
|
|
c09d2fad3e | ||
|
|
7928f7fb30 | ||
|
|
438289b2ed | ||
|
|
561fc67f33 | ||
|
|
aa7767f37c | ||
|
|
f23792881e | ||
|
|
c920b28acc | ||
|
|
36f1b4d5be | ||
|
|
141bec559f | ||
|
|
0d885e6fa0 | ||
|
|
e10f5277e9 | ||
|
|
f33768fe32 | ||
|
|
1e565768d1 | ||
|
|
5193fef888 | ||
|
|
63930c4b33 | ||
|
|
ac27034542 | ||
|
|
cfe9345b53 | ||
|
|
1e2d736d6d | ||
|
|
0c825251eb | ||
|
|
ccf00b7d06 | ||
|
|
6d412a7bea | ||
|
|
5a0d3054b8 | ||
|
|
6dbc6d2d07 | ||
|
|
ea8e1b1076 | ||
|
|
d1368791e9 | ||
|
|
590166f847 | ||
|
|
19d0df7e7f | ||
|
|
1d87c42977 | ||
|
|
ec7505987d | ||
|
|
37729269ba | ||
|
|
952bcde25f | ||
|
|
4848df4faf | ||
|
|
41d1f9d26f | ||
|
|
113adb5b85 | ||
|
|
8e5aa35bf3 | ||
|
|
1139a37338 | ||
|
|
b20e51561a | ||
|
|
5ff44bbae8 | ||
|
|
2beeb6f644 | ||
|
|
3eede1bf43 | ||
|
|
9f8ea3a6b5 | ||
|
|
23679d119b | ||
|
|
a3fd6008a0 | ||
|
|
d0014b3f8b | ||
|
|
04c96eb2ff | ||
|
|
df64c750cc | ||
|
|
b90b2ac0bf | ||
|
|
39a1755b3d | ||
|
|
61fb6898c0 | ||
|
|
bc34f140c8 | ||
|
|
0bee2be3cf | ||
|
|
1d67172dd3 | ||
|
|
b99a97eb48 | ||
|
|
a1899a719f | ||
|
|
5fcfa8f369 | ||
|
|
7172d1f701 | ||
|
|
8bc760855e | ||
|
|
5fc3683ebb | ||
|
|
626416a202 | ||
|
|
d84afb939a | ||
|
|
3cd875d6ee | ||
|
|
f5e63b7cbd | ||
|
|
8b20f44dd5 | ||
|
|
59a3a22ea5 | ||
|
|
2f34b7e83b | ||
|
|
4d5ec6cac1 | ||
|
|
7635109f6d | ||
|
|
de60b7f952 | ||
|
|
405261a67e | ||
|
|
4bcec73e07 | ||
|
|
d81c00c0b0 | ||
|
|
d2e4f12ea3 | ||
|
|
f7c79166da | ||
|
|
2b5337329a | ||
|
|
21206b670c | ||
|
|
1d04a13a64 | ||
|
|
9e309584db |
163 changed files with 71002 additions and 13808 deletions
2
LICENSE
2
LICENSE
|
|
@ -186,7 +186,7 @@
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2017-2021 Intel Corporation
|
Copyright 2017-2025 Intel Corporation
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
||||||
|
|
@ -594,12 +594,14 @@
|
||||||
<Content Include="readme.md" />
|
<Content Include="readme.md" />
|
||||||
<Content Include="sample-config-advanced.json" />
|
<Content Include="sample-config-advanced.json" />
|
||||||
<Content Include="sample-config.json" />
|
<Content Include="sample-config.json" />
|
||||||
|
<Content Include="SECURITY.md" />
|
||||||
<Content Include="SourceFileList.txt" />
|
<Content Include="SourceFileList.txt" />
|
||||||
<Content Include="translate\readme.txt" />
|
<Content Include="translate\readme.txt" />
|
||||||
<Content Include="translate\translate.json" />
|
<Content Include="translate\translate.json" />
|
||||||
<Content Include="views\agentinvite.handlebars" />
|
<Content Include="views\agentinvite.handlebars" />
|
||||||
<Content Include="views\default-mobile.handlebars" />
|
<Content Include="views\default-mobile.handlebars" />
|
||||||
<Content Include="views\default.handlebars" />
|
<Content Include="views\default.handlebars" />
|
||||||
|
<Content Include="views\default3.handlebars" />
|
||||||
<Content Include="views\download.handlebars" />
|
<Content Include="views\download.handlebars" />
|
||||||
<Content Include="views\download2.handlebars" />
|
<Content Include="views\download2.handlebars" />
|
||||||
<Content Include="views\error404-mobile.handlebars" />
|
<Content Include="views\error404-mobile.handlebars" />
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -588,7 +588,7 @@ function run(argv) {
|
||||||
}
|
}
|
||||||
amtMei.getProvisioningState(function (result) { if (result) { mestate.ProvisioningState = result; } });
|
amtMei.getProvisioningState(function (result) { if (result) { mestate.ProvisioningState = result; } });
|
||||||
amtMei.getProvisioningMode(function (result) { if (result) { mestate.ProvisioningMode = result; } });
|
amtMei.getProvisioningMode(function (result) { if (result) { mestate.ProvisioningMode = result; } });
|
||||||
amtMei.getEHBCState(function (result) { mestate.ehbc = ((result === true) || (typeof result == 'object') && (result.EHBC === true)); });
|
amtMei.getEHBCState(function (result) { if (result) { mestate.ehbc = ((result === true) || (typeof result == 'object') && (result.EHBC === true)); } });
|
||||||
amtMei.getControlMode(function (result) { if (result) { mestate.controlmode = result; } });
|
amtMei.getControlMode(function (result) { if (result) { mestate.controlmode = result; } });
|
||||||
amtMei.getMACAddresses(function (result) { if (result) { mestate.mac = result; } });
|
amtMei.getMACAddresses(function (result) { if (result) { mestate.mac = result; } });
|
||||||
amtMei.getLanInterfaceSettings(0, function (result) { if (result) { mestate.net0 = result; } });
|
amtMei.getLanInterfaceSettings(0, function (result) { if (result) { mestate.net0 = result; } });
|
||||||
|
|
|
||||||
|
|
@ -295,8 +295,9 @@ if (process.platform == 'win32' && require('user-sessions').isRoot()) {
|
||||||
// Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value
|
// Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value
|
||||||
try {
|
try {
|
||||||
var writtenSize = 0, actualSize = Math.floor(require('fs').statSync(process.execPath).size / 1024);
|
var writtenSize = 0, actualSize = Math.floor(require('fs').statSync(process.execPath).size / 1024);
|
||||||
try { writtenSize = require('win-registry').QueryKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MeshCentralAgent', 'EstimatedSize'); } catch (ex) { }
|
var serviceName = (_MSH().serviceName ? _MSH().serviceName : (require('_agentNodeId').serviceName() ? require('_agentNodeId').serviceName() : 'Mesh Agent'));
|
||||||
if (writtenSize != actualSize) { try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MeshCentralAgent', 'EstimatedSize', actualSize); } catch (ex) { } }
|
try { writtenSize = require('win-registry').QueryKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'EstimatedSize'); } catch (ex) { }
|
||||||
|
if (writtenSize != actualSize) { try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'EstimatedSize', actualSize); } catch (ex) { } }
|
||||||
} catch (ex) { }
|
} catch (ex) { }
|
||||||
|
|
||||||
// Check to see if we are the Installed Mesh Agent Service, if we are, make sure we can run in Safe Mode
|
// Check to see if we are the Installed Mesh Agent Service, if we are, make sure we can run in Safe Mode
|
||||||
|
|
@ -310,6 +311,16 @@ if (process.platform == 'win32' && require('user-sessions').isRoot()) {
|
||||||
try { meshCheck = require('service-manager').manager.getService(svcname).isMe(); } catch (ex) { }
|
try { meshCheck = require('service-manager').manager.getService(svcname).isMe(); } catch (ex) { }
|
||||||
if (meshCheck && require('win-bcd').isSafeModeService && !require('win-bcd').isSafeModeService(svcname)) { require('win-bcd').enableSafeModeService(svcname); }
|
if (meshCheck && require('win-bcd').isSafeModeService && !require('win-bcd').isSafeModeService(svcname)) { require('win-bcd').enableSafeModeService(svcname); }
|
||||||
} catch (ex) { }
|
} catch (ex) { }
|
||||||
|
|
||||||
|
// Check the Agent Uninstall MetaData for DisplayVersion and update if not the same and only on windows
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
try {
|
||||||
|
var writtenDisplayVersion = 0, actualDisplayVersion = process.versions.commitDate.toString();
|
||||||
|
var serviceName = (_MSH().serviceName ? _MSH().serviceName : (require('_agentNodeId').serviceName() ? require('_agentNodeId').serviceName() : 'Mesh Agent'));
|
||||||
|
try { writtenDisplayVersion = require('win-registry').QueryKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'DisplayVersion'); } catch (ex) { }
|
||||||
|
if (writtenDisplayVersion != actualDisplayVersion) { try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'DisplayVersion', actualDisplayVersion); } catch (ex) { } }
|
||||||
|
} catch (ex) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform != 'win32') {
|
if (process.platform != 'win32') {
|
||||||
|
|
@ -655,33 +666,39 @@ var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ?
|
||||||
try { require('os').name().then(function (v) { meshCoreObj.osdesc = v; meshCoreObjChanged(); }); } catch (ex) { }
|
try { require('os').name().then(function (v) { meshCoreObj.osdesc = v; meshCoreObjChanged(); }); } catch (ex) { }
|
||||||
|
|
||||||
// Setup logged in user monitoring (THIS IS BROKEN IN WIN7)
|
// Setup logged in user monitoring (THIS IS BROKEN IN WIN7)
|
||||||
|
function onUserSessionChanged(user, locked) {
|
||||||
|
userSession.enumerateUsers().then(function (users) {
|
||||||
|
if (process.platform == 'linux') {
|
||||||
|
if (userSession._startTime == null) {
|
||||||
|
userSession._startTime = Date.now();
|
||||||
|
userSession._count = users.length;
|
||||||
|
}
|
||||||
|
else if (Date.now() - userSession._startTime < 10000 && users.length == userSession._count) {
|
||||||
|
userSession.removeAllListeners('changed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var u = [], a = users.Active;
|
||||||
|
if(meshCoreObj.lusers == null) { meshCoreObj.lusers = []; }
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
var un = a[i].Domain ? (a[i].Domain + '\\' + a[i].Username) : (a[i].Username);
|
||||||
|
if (user && locked && (JSON.stringify(a[i]) === JSON.stringify(user))) { if (meshCoreObj.lusers.indexOf(un) == -1) { meshCoreObj.lusers.push(un); } }
|
||||||
|
else if (user && !locked && (JSON.stringify(a[i]) === JSON.stringify(user))) { meshCoreObj.lusers.splice(meshCoreObj.lusers.indexOf(un), 1); }
|
||||||
|
if (u.indexOf(un) == -1) { u.push(un); } // Only push users in the list once.
|
||||||
|
}
|
||||||
|
meshCoreObj.lusers = meshCoreObj.lusers;
|
||||||
|
meshCoreObj.users = u;
|
||||||
|
meshCoreObjChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var userSession = require('user-sessions');
|
var userSession = require('user-sessions');
|
||||||
userSession.on('changed', function onUserSessionChanged() {
|
userSession.on('changed', function () { onUserSessionChanged(null, false); });
|
||||||
userSession.enumerateUsers().then(function (users) {
|
|
||||||
if (process.platform == 'linux') {
|
|
||||||
if (userSession._startTime == null) {
|
|
||||||
userSession._startTime = Date.now();
|
|
||||||
userSession._count = users.length;
|
|
||||||
}
|
|
||||||
else if (Date.now() - userSession._startTime < 10000 && users.length == userSession._count) {
|
|
||||||
userSession.removeAllListeners('changed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var u = [], a = users.Active;
|
|
||||||
for (var i = 0; i < a.length; i++) {
|
|
||||||
var un = a[i].Domain ? (a[i].Domain + '\\' + a[i].Username) : (a[i].Username);
|
|
||||||
if (u.indexOf(un) == -1) { u.push(un); } // Only push users in the list once.
|
|
||||||
}
|
|
||||||
meshCoreObj.users = u;
|
|
||||||
meshCoreObjChanged();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
userSession.emit('changed');
|
userSession.emit('changed');
|
||||||
//userSession.on('locked', function (user) { sendConsoleText('[' + (user.Domain ? user.Domain + '\\' : '') + user.Username + '] has LOCKED the desktop'); });
|
userSession.on('locked', function (user) { if(user != undefined && user != null) { onUserSessionChanged(user, true); } });
|
||||||
//userSession.on('unlocked', function (user) { sendConsoleText('[' + (user.Domain ? user.Domain + '\\' : '') + user.Username + '] has UNLOCKED the desktop'); });
|
userSession.on('unlocked', function (user) { if(user != undefined && user != null) { onUserSessionChanged(user, false); } });
|
||||||
} catch (ex) { }
|
} catch (ex) { }
|
||||||
|
|
||||||
var meshServerConnectionState = 0;
|
var meshServerConnectionState = 0;
|
||||||
|
|
@ -1158,6 +1175,8 @@ function handleServerCommand(data) {
|
||||||
tunnel.soptions = data.soptions;
|
tunnel.soptions = data.soptions;
|
||||||
tunnel.consentTimeout = (tunnel.soptions && tunnel.soptions.consentTimeout) ? tunnel.soptions.consentTimeout : 30;
|
tunnel.consentTimeout = (tunnel.soptions && tunnel.soptions.consentTimeout) ? tunnel.soptions.consentTimeout : 30;
|
||||||
tunnel.consentAutoAccept = (tunnel.soptions && (tunnel.soptions.consentAutoAccept === true));
|
tunnel.consentAutoAccept = (tunnel.soptions && (tunnel.soptions.consentAutoAccept === true));
|
||||||
|
tunnel.consentAutoAcceptIfNoUser = (tunnel.soptions && (tunnel.soptions.consentAutoAcceptIfNoUser === true));
|
||||||
|
tunnel.oldStyle = (tunnel.soptions && tunnel.soptions.oldStyle) ? tunnel.soptions.oldStyle : false;
|
||||||
tunnel.tcpaddr = data.tcpaddr;
|
tunnel.tcpaddr = data.tcpaddr;
|
||||||
tunnel.tcpport = data.tcpport;
|
tunnel.tcpport = data.tcpport;
|
||||||
tunnel.udpaddr = data.udpaddr;
|
tunnel.udpaddr = data.udpaddr;
|
||||||
|
|
@ -1571,7 +1590,7 @@ function handleServerCommand(data) {
|
||||||
mesh.cmdchild = require('child_process').execFile('/bin/sh', ['sh'], options);
|
mesh.cmdchild = require('child_process').execFile('/bin/sh', ['sh'], options);
|
||||||
mesh.cmdchild.descriptorMetadata = 'UserCommandsShell';
|
mesh.cmdchild.descriptorMetadata = 'UserCommandsShell';
|
||||||
mesh.cmdchild.stdout.on('data', function (c) { replydata += c.toString(); });
|
mesh.cmdchild.stdout.on('data', function (c) { replydata += c.toString(); });
|
||||||
mesh.cmdchild.stderr.on('data', function (c) { replydata + c.toString(); });
|
mesh.cmdchild.stderr.on('data', function (c) { replydata += c.toString(); });
|
||||||
mesh.cmdchild.stdin.write(data.cmds.split('\r').join('') + '\nexit\n');
|
mesh.cmdchild.stdin.write(data.cmds.split('\r').join('') + '\nexit\n');
|
||||||
mesh.cmdchild.on('exit', function () {
|
mesh.cmdchild.on('exit', function () {
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
|
|
@ -1935,9 +1954,9 @@ function getSystemInformation(func) {
|
||||||
if (process.platform == 'win32')
|
if (process.platform == 'win32')
|
||||||
{
|
{
|
||||||
results.pendingReboot = require('win-info').pendingReboot(); // Pending reboot
|
results.pendingReboot = require('win-info').pendingReboot(); // Pending reboot
|
||||||
if (require('computer-identifiers').volumes_promise != null)
|
if (require('win-volumes').volumes_promise != null)
|
||||||
{
|
{
|
||||||
var p = require('computer-identifiers').volumes_promise();
|
var p = require('win-volumes').volumes_promise();
|
||||||
p.then(function (res)
|
p.then(function (res)
|
||||||
{
|
{
|
||||||
results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(res);
|
results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(res);
|
||||||
|
|
@ -1945,12 +1964,6 @@ function getSystemInformation(func) {
|
||||||
func(results);
|
func(results);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (require('computer-identifiers').volumes != null)
|
|
||||||
{
|
|
||||||
results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(require('computer-identifiers').volumes());
|
|
||||||
results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex');
|
|
||||||
func(results);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex');
|
results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex');
|
||||||
|
|
@ -2302,6 +2315,59 @@ function terminal_end()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function terminal_consent_ask(ws) {
|
||||||
|
ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
||||||
|
var consentMessage = currentTranslation['terminalConsent'].replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username);
|
||||||
|
var consentTitle = 'MeshCentral';
|
||||||
|
if (ws.httprequest.soptions != null) {
|
||||||
|
if (ws.httprequest.soptions.consentTitle != null) { consentTitle = ws.httprequest.soptions.consentTitle; }
|
||||||
|
if (ws.httprequest.soptions.consentMsgTerminal != null) { consentMessage = ws.httprequest.soptions.consentMsgTerminal.replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username); }
|
||||||
|
}
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
var enhanced = false;
|
||||||
|
if (ws.httprequest.oldStyle === false) {
|
||||||
|
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
||||||
|
}
|
||||||
|
if (enhanced) {
|
||||||
|
var ipr = server_getUserImage(ws.httprequest.userid);
|
||||||
|
ipr.consentTitle = consentTitle;
|
||||||
|
ipr.consentMessage = consentMessage;
|
||||||
|
ipr.consentTimeout = ws.httprequest.consentTimeout;
|
||||||
|
ipr.consentAutoAccept = ws.httprequest.consentAutoAccept;
|
||||||
|
ipr.username = ws.httprequest.realname;
|
||||||
|
ipr.tsid = ws.tsid;
|
||||||
|
ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
||||||
|
ws.httprequest.tpromise._consent = ipr.then(function (img) {
|
||||||
|
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground });
|
||||||
|
this.__childPromise.close = this.consent.close.bind(this.consent);
|
||||||
|
return (this.consent);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ws.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ws.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout);
|
||||||
|
}
|
||||||
|
ws.httprequest.tpromise._consent.retPromise = ws.httprequest.tpromise;
|
||||||
|
ws.httprequest.tpromise._consent.then(function (always) {
|
||||||
|
if (always && process.platform == 'win32') { server_set_consentTimer(this.retPromise.httprequest.userid); }
|
||||||
|
// Success
|
||||||
|
MeshServerLogEx(27, null, "Local user accepted remote terminal request (" + this.retPromise.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
|
||||||
|
this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 }));
|
||||||
|
this.retPromise._consent = null;
|
||||||
|
this.retPromise._res();
|
||||||
|
}, function (e) {
|
||||||
|
if (this.retPromise.that) {
|
||||||
|
if(this.retPromise.that.httprequest){ // User Consent Denied
|
||||||
|
MeshServerLogEx(28, null, "Local user rejected remote terminal request (" + this.retPromise.that.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
|
||||||
|
} else { } // Connection was closed server side, maybe log some messages somewhere?
|
||||||
|
this.retPromise._consent = null;
|
||||||
|
this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
|
||||||
|
} else { } // no websocket, maybe log some messages somewhere?
|
||||||
|
this.retPromise._rej(e.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function terminal_promise_connection_rejected(e)
|
function terminal_promise_connection_rejected(e)
|
||||||
{
|
{
|
||||||
// FAILED to connect terminal
|
// FAILED to connect terminal
|
||||||
|
|
@ -2614,6 +2680,101 @@ function kvm_tunnel_consentpromise_closehandler()
|
||||||
if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); }
|
if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function kvm_consent_ok(ws) {
|
||||||
|
// User Consent Prompt is not required because no user is present
|
||||||
|
if (ws.httprequest.consent && (ws.httprequest.consent & 1)){
|
||||||
|
// User Notifications is required
|
||||||
|
MeshServerLogEx(35, null, "Started remote desktop with toast notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', ws.httprequest.realname);
|
||||||
|
var notifyTitle = "MeshCentral";
|
||||||
|
if (ws.httprequest.soptions != null) {
|
||||||
|
if (ws.httprequest.soptions.notifyTitle != null) { notifyTitle = ws.httprequest.soptions.notifyTitle; }
|
||||||
|
if (ws.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = ws.httprequest.soptions.notifyMsgDesktop.replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username); }
|
||||||
|
}
|
||||||
|
try { require('toaster').Toast(notifyTitle, notifyMessage, ws.tsid); } catch (ex) { }
|
||||||
|
} else {
|
||||||
|
MeshServerLogEx(36, null, "Started remote desktop without notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
}
|
||||||
|
if (ws.httprequest.consent && (ws.httprequest.consent & 0x40)) {
|
||||||
|
// Connection Bar is required
|
||||||
|
if (ws.httprequest.desktop.kvm.connectionBar) {
|
||||||
|
ws.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
|
||||||
|
ws.httprequest.desktop.kvm.connectionBar.close();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ws.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(ws.httprequest.privacybartext.replace('{0}', ws.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', ws.httprequest.desktop.kvm.users.join(', ')).replace(/'/g, "\\'\\"), require('MeshAgent')._tsid, color_options);
|
||||||
|
MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
} catch (ex) {
|
||||||
|
MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or not Supported (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
}
|
||||||
|
if (ws.httprequest.desktop.kvm.connectionBar) {
|
||||||
|
ws.httprequest.desktop.kvm.connectionBar.state = {
|
||||||
|
userid: ws.httprequest.userid,
|
||||||
|
xuserid: ws.httprequest.xuserid,
|
||||||
|
username: ws.httprequest.username,
|
||||||
|
sessionid: ws.httprequest.sessionid,
|
||||||
|
remoteaddr: ws.httprequest.remoteaddr,
|
||||||
|
guestname: ws.httprequest.guestname,
|
||||||
|
desktop: ws.httprequest.desktop
|
||||||
|
};
|
||||||
|
ws.httprequest.desktop.kvm.connectionBar.on('close', function () {
|
||||||
|
console.info1('Connection Bar Forcefully closed');
|
||||||
|
MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", this.state);
|
||||||
|
for (var i in this.state.desktop.kvm._pipedStreams) {
|
||||||
|
this.state.desktop.kvm._pipedStreams[i].end();
|
||||||
|
}
|
||||||
|
this.state.desktop.kvm.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.httprequest.desktop.kvm.pipe(ws, { dataTypeSkip: 1 });
|
||||||
|
if (ws.httprequest.autolock) {
|
||||||
|
destopLockHelper_pipe(ws.httprequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function kvm_consent_ask(ws){
|
||||||
|
// Send a console message back using the console channel, "\n" is supported.
|
||||||
|
ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
||||||
|
var consentMessage = currentTranslation['desktopConsent'].replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username);
|
||||||
|
var consentTitle = 'MeshCentral';
|
||||||
|
if (ws.httprequest.soptions != null) {
|
||||||
|
if (ws.httprequest.soptions.consentTitle != null) { consentTitle = ws.httprequest.soptions.consentTitle; }
|
||||||
|
if (ws.httprequest.soptions.consentMsgDesktop != null) { consentMessage = ws.httprequest.soptions.consentMsgDesktop.replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username); }
|
||||||
|
}
|
||||||
|
var pr;
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
var enhanced = false;
|
||||||
|
if (ws.httprequest.oldStyle === false) {
|
||||||
|
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
||||||
|
}
|
||||||
|
if (enhanced) {
|
||||||
|
var ipr = server_getUserImage(ws.httprequest.userid);
|
||||||
|
ipr.consentTitle = consentTitle;
|
||||||
|
ipr.consentMessage = consentMessage;
|
||||||
|
ipr.consentTimeout = ws.httprequest.consentTimeout;
|
||||||
|
ipr.consentAutoAccept = ws.httprequest.consentAutoAccept;
|
||||||
|
ipr.tsid = ws.tsid;
|
||||||
|
ipr.username = ws.httprequest.realname;
|
||||||
|
ipr.translation = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
||||||
|
pr = ipr.then(function (img) {
|
||||||
|
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translation, background: color_options.background, foreground: color_options.foreground });
|
||||||
|
this.__childPromise.close = this.consent.close.bind(this.consent);
|
||||||
|
return (this.consent);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null, ws.tsid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null, ws.tsid);
|
||||||
|
}
|
||||||
|
pr.ws = ws;
|
||||||
|
ws.pause();
|
||||||
|
ws._consentpromise = pr;
|
||||||
|
ws.prependOnceListener('end', kvm_tunnel_consentpromise_closehandler);
|
||||||
|
pr.then(kvm_consentpromise_resolved, kvm_consentpromise_rejected);
|
||||||
|
}
|
||||||
|
|
||||||
function kvm_consentpromise_rejected(e)
|
function kvm_consentpromise_rejected(e)
|
||||||
{
|
{
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
|
|
@ -2693,6 +2854,67 @@ function kvm_consentpromise_resolved(always)
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function files_consent_ok(ws){
|
||||||
|
// User Consent Prompt is not required
|
||||||
|
if (ws.httprequest.consent && (ws.httprequest.consent & 4)) {
|
||||||
|
// User Notifications is required
|
||||||
|
MeshServerLogEx(42, null, "Started remote files with toast notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
var notifyMessage = currentTranslation['fileNotify'].replace('{0}', ws.httprequest.realname);
|
||||||
|
var notifyTitle = "MeshCentral";
|
||||||
|
if (ws.httprequest.soptions != null) {
|
||||||
|
if (ws.httprequest.soptions.notifyTitle != null) { notifyTitle = ws.httprequest.soptions.notifyTitle; }
|
||||||
|
if (ws.httprequest.soptions.notifyMsgFiles != null) { notifyMessage = ws.httprequest.soptions.notifyMsgFiles.replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username); }
|
||||||
|
}
|
||||||
|
try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
|
||||||
|
} else {
|
||||||
|
MeshServerLogEx(43, null, "Started remote files without notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest);
|
||||||
|
}
|
||||||
|
ws.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
function files_consent_ask(ws){
|
||||||
|
// Send a console message back using the console channel, "\n" is supported.
|
||||||
|
ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
||||||
|
var consentMessage = currentTranslation['fileConsent'].replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username);
|
||||||
|
var consentTitle = 'MeshCentral';
|
||||||
|
|
||||||
|
if (ws.httprequest.soptions != null) {
|
||||||
|
if (ws.httprequest.soptions.consentTitle != null) { consentTitle = ws.httprequest.soptions.consentTitle; }
|
||||||
|
if (ws.httprequest.soptions.consentMsgFiles != null) { consentMessage = ws.httprequest.soptions.consentMsgFiles.replace('{0}', ws.httprequest.realname).replace('{1}', ws.httprequest.username); }
|
||||||
|
}
|
||||||
|
var pr;
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
var enhanced = false;
|
||||||
|
if (ws.httprequest.oldStyle === false) {
|
||||||
|
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
||||||
|
}
|
||||||
|
if (enhanced) {
|
||||||
|
var ipr = server_getUserImage(ws.httprequest.userid);
|
||||||
|
ipr.consentTitle = consentTitle;
|
||||||
|
ipr.consentMessage = consentMessage;
|
||||||
|
ipr.consentTimeout = ws.httprequest.consentTimeout;
|
||||||
|
ipr.consentAutoAccept = ws.httprequest.consentAutoAccept;
|
||||||
|
ipr.username = ws.httprequest.realname;
|
||||||
|
ipr.tsid = ws.tsid;
|
||||||
|
ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
||||||
|
pr = ipr.then(function (img) {
|
||||||
|
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground });
|
||||||
|
this.__childPromise.close = this.consent.close.bind(this.consent);
|
||||||
|
return (this.consent);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null);
|
||||||
|
}
|
||||||
|
pr.ws = ws;
|
||||||
|
ws.pause();
|
||||||
|
ws._consentpromise = pr;
|
||||||
|
ws.prependOnceListener('end', files_tunnel_endhandler);
|
||||||
|
pr.then(files_consentpromise_resolved, files_consentpromise_rejected);
|
||||||
|
}
|
||||||
|
|
||||||
function files_consentpromise_resolved(always)
|
function files_consentpromise_resolved(always)
|
||||||
{
|
{
|
||||||
if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); }
|
if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); }
|
||||||
|
|
@ -2806,6 +3028,12 @@ function onTunnelData(data)
|
||||||
|
|
||||||
this.descriptorMetadata = "Remote Terminal";
|
this.descriptorMetadata = "Remote Terminal";
|
||||||
|
|
||||||
|
// Look for a TSID
|
||||||
|
var tsid = null;
|
||||||
|
if ((this.httprequest.xoptions != null) && (typeof this.httprequest.xoptions.tsid == 'number')) { tsid = this.httprequest.xoptions.tsid; }
|
||||||
|
require('MeshAgent')._tsid = tsid;
|
||||||
|
this.tsid = tsid;
|
||||||
|
|
||||||
if (process.platform == 'win32')
|
if (process.platform == 'win32')
|
||||||
{
|
{
|
||||||
if (!require('win-terminal').PowerShellCapable() && (this.httprequest.protocol == 6 || this.httprequest.protocol == 9)) {
|
if (!require('win-terminal').PowerShellCapable() && (this.httprequest.protocol == 6 || this.httprequest.protocol == 9)) {
|
||||||
|
|
@ -2822,74 +3050,31 @@ function onTunnelData(data)
|
||||||
this.end = terminal_end;
|
this.end = terminal_end;
|
||||||
|
|
||||||
// Perform User-Consent if needed.
|
// Perform User-Consent if needed.
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 16))
|
if (this.httprequest.consent && (this.httprequest.consent & 16)) {
|
||||||
{
|
// User asked for consent so now we check if we can auto accept if no user is present/loggedin
|
||||||
this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
if (this.httprequest.consentAutoAcceptIfNoUser) {
|
||||||
var consentMessage = currentTranslation['terminalConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
|
var p = require('user-sessions').enumerateUsers();
|
||||||
var consentTitle = 'MeshCentral';
|
p.sessionid = this.httprequest.sessionid;
|
||||||
|
p.ws = this;
|
||||||
if (this.httprequest.soptions != null)
|
p.then(function (u) {
|
||||||
{
|
var v = [];
|
||||||
if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
|
for (var i in u) {
|
||||||
if (this.httprequest.soptions.consentMsgTerminal != null) { consentMessage = this.httprequest.soptions.consentMsgTerminal.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
|
if (u[i].State == 'Active') { v.push({ tsid: i, type: u[i].StationName, user: u[i].Username, domain: u[i].Domain }); }
|
||||||
}
|
}
|
||||||
if (process.platform == 'win32')
|
if (v.length == 0) { // No user is present, auto accept
|
||||||
{
|
this.ws.httprequest.tpromise._res();
|
||||||
var enhanced = false;
|
} else {
|
||||||
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
// User is present so we still need consent
|
||||||
if (enhanced)
|
terminal_consent_ask(this.ws);
|
||||||
{
|
}
|
||||||
var ipr = server_getUserImage(this.httprequest.userid);
|
|
||||||
ipr.consentTitle = consentTitle;
|
|
||||||
ipr.consentMessage = consentMessage;
|
|
||||||
ipr.consentTimeout = this.httprequest.consentTimeout;
|
|
||||||
ipr.consentAutoAccept = this.httprequest.consentAutoAccept;
|
|
||||||
ipr.username = this.httprequest.realname;
|
|
||||||
ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
|
||||||
this.httprequest.tpromise._consent = ipr.then(function (img)
|
|
||||||
{
|
|
||||||
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground });
|
|
||||||
this.__childPromise.close = this.consent.close.bind(this.consent);
|
|
||||||
return (this.consent);
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout);
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout);
|
|
||||||
}
|
|
||||||
this.httprequest.tpromise._consent.retPromise = this.httprequest.tpromise;
|
|
||||||
this.httprequest.tpromise._consent.then(
|
|
||||||
function (always)
|
|
||||||
{
|
|
||||||
if (always && process.platform == 'win32') { server_set_consentTimer(this.retPromise.httprequest.userid); }
|
|
||||||
|
|
||||||
// Success
|
|
||||||
MeshServerLogEx(27, null, "Local user accepted remote terminal request (" + this.retPromise.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
|
|
||||||
this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 }));
|
|
||||||
this.retPromise._consent = null;
|
|
||||||
this.retPromise._res();
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (this.retPromise.that) {
|
|
||||||
if(this.retPromise.that.httprequest){ // User Consent Denied
|
|
||||||
MeshServerLogEx(28, null, "Local user rejected remote terminal request (" + this.retPromise.that.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
|
|
||||||
} else { } // Connection was closed server side, maybe log some messages somewhere?
|
|
||||||
this.retPromise._consent = null;
|
|
||||||
this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
|
|
||||||
} else { } // no websocket, maybe log some messages somewhere?
|
|
||||||
this.retPromise._rej(e.toString());
|
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else
|
terminal_consent_ask(this);
|
||||||
{
|
}
|
||||||
|
} else {
|
||||||
// User-Consent is not required, so just resolve this promise
|
// User-Consent is not required, so just resolve this promise
|
||||||
this.httprequest.tpromise._res();
|
this.httprequest.tpromise._res();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.httprequest.tpromise.then(terminal_promise_consent_resolved, terminal_promise_consent_rejected);
|
this.httprequest.tpromise.then(terminal_promise_consent_resolved, terminal_promise_consent_rejected);
|
||||||
}
|
}
|
||||||
else if (this.httprequest.protocol == 2)
|
else if (this.httprequest.protocol == 2)
|
||||||
|
|
@ -2913,6 +3098,7 @@ function onTunnelData(data)
|
||||||
var tsid = null;
|
var tsid = null;
|
||||||
if ((this.httprequest.xoptions != null) && (typeof this.httprequest.xoptions.tsid == 'number')) { tsid = this.httprequest.xoptions.tsid; }
|
if ((this.httprequest.xoptions != null) && (typeof this.httprequest.xoptions.tsid == 'number')) { tsid = this.httprequest.xoptions.tsid; }
|
||||||
require('MeshAgent')._tsid = tsid;
|
require('MeshAgent')._tsid = tsid;
|
||||||
|
this.tsid = tsid;
|
||||||
|
|
||||||
// If MacOS, Wake up device with caffeinate
|
// If MacOS, Wake up device with caffeinate
|
||||||
if(process.platform == 'darwin'){
|
if(process.platform == 'darwin'){
|
||||||
|
|
@ -2984,117 +3170,33 @@ function onTunnelData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform notification if needed. Toast messages may not be supported on all platforms.
|
// Perform notification if needed. Toast messages may not be supported on all platforms.
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 8))
|
if (this.httprequest.consent && (this.httprequest.consent & 8)) {
|
||||||
{
|
|
||||||
// User Consent Prompt is required
|
// User asked for consent but now we check if can auto accept if no user is present
|
||||||
// Send a console message back using the console channel, "\n" is supported.
|
if (this.httprequest.consentAutoAcceptIfNoUser) {
|
||||||
this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
// Get list of users to check if we any actual users logged in, and if users logged in, we still need consent
|
||||||
var consentMessage = currentTranslation['desktopConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
|
var p = require('user-sessions').enumerateUsers();
|
||||||
var consentTitle = 'MeshCentral';
|
p.sessionid = this.httprequest.sessionid;
|
||||||
if (this.httprequest.soptions != null)
|
p.ws = this;
|
||||||
{
|
p.then(function (u) {
|
||||||
if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
|
var v = [];
|
||||||
if (this.httprequest.soptions.consentMsgDesktop != null) { consentMessage = this.httprequest.soptions.consentMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
|
for (var i in u) {
|
||||||
|
if (u[i].State == 'Active') { v.push({ tsid: i, type: u[i].StationName, user: u[i].Username, domain: u[i].Domain }); }
|
||||||
|
}
|
||||||
|
if (v.length == 0) { // No user is present, auto accept
|
||||||
|
kvm_consent_ok(this.ws);
|
||||||
|
} else {
|
||||||
|
// User is present so we still need consent
|
||||||
|
kvm_consent_ask(this.ws);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// User Consent Prompt is required
|
||||||
|
kvm_consent_ask(this);
|
||||||
}
|
}
|
||||||
var pr;
|
} else {
|
||||||
if (process.platform == 'win32')
|
|
||||||
{
|
|
||||||
var enhanced = false;
|
|
||||||
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
|
||||||
if (enhanced)
|
|
||||||
{
|
|
||||||
var ipr = server_getUserImage(this.httprequest.userid);
|
|
||||||
ipr.consentTitle = consentTitle;
|
|
||||||
ipr.consentMessage = consentMessage;
|
|
||||||
ipr.consentTimeout = this.httprequest.consentTimeout;
|
|
||||||
ipr.consentAutoAccept = this.httprequest.consentAutoAccept;
|
|
||||||
ipr.tsid = tsid;
|
|
||||||
ipr.username = this.httprequest.realname;
|
|
||||||
ipr.translation = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
|
||||||
pr = ipr.then(function (img)
|
|
||||||
{
|
|
||||||
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translation, background: color_options.background, foreground: color_options.foreground });
|
|
||||||
this.__childPromise.close = this.consent.close.bind(this.consent);
|
|
||||||
return (this.consent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid);
|
|
||||||
}
|
|
||||||
pr.ws = this;
|
|
||||||
this.pause();
|
|
||||||
this._consentpromise = pr;
|
|
||||||
this.prependOnceListener('end', kvm_tunnel_consentpromise_closehandler);
|
|
||||||
pr.then(kvm_consentpromise_resolved, kvm_consentpromise_rejected);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// User Consent Prompt is not required
|
// User Consent Prompt is not required
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 1))
|
kvm_consent_ok(this);
|
||||||
{
|
|
||||||
// User Notifications is required
|
|
||||||
MeshServerLogEx(35, null, "Started remote desktop with toast notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
|
||||||
var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.httprequest.realname);
|
|
||||||
var notifyTitle = "MeshCentral";
|
|
||||||
if (this.httprequest.soptions != null) {
|
|
||||||
if (this.httprequest.soptions.notifyTitle != null) { notifyTitle = this.httprequest.soptions.notifyTitle; }
|
|
||||||
if (this.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = this.httprequest.soptions.notifyMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
|
|
||||||
}
|
|
||||||
try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { }
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
MeshServerLogEx(36, null, "Started remote desktop without notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
|
||||||
}
|
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 0x40))
|
|
||||||
{
|
|
||||||
// Connection Bar is required
|
|
||||||
if (this.httprequest.desktop.kvm.connectionBar)
|
|
||||||
{
|
|
||||||
this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
|
|
||||||
this.httprequest.desktop.kvm.connectionBar.close();
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.httprequest.privacybartext.replace('{0}', this.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.httprequest.desktop.kvm.users.join(', ')).replace(/'/g, "\\'\\"), require('MeshAgent')._tsid, color_options);
|
|
||||||
MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
|
||||||
} catch (ex) {
|
|
||||||
MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or not Supported (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
|
||||||
}
|
|
||||||
if (this.httprequest.desktop.kvm.connectionBar)
|
|
||||||
{
|
|
||||||
this.httprequest.desktop.kvm.connectionBar.state =
|
|
||||||
{
|
|
||||||
userid: this.httprequest.userid,
|
|
||||||
xuserid: this.httprequest.xuserid,
|
|
||||||
username: this.httprequest.username,
|
|
||||||
sessionid: this.httprequest.sessionid,
|
|
||||||
remoteaddr: this.httprequest.remoteaddr,
|
|
||||||
guestname: this.httprequest.guestname,
|
|
||||||
desktop: this.httprequest.desktop
|
|
||||||
};
|
|
||||||
this.httprequest.desktop.kvm.connectionBar.on('close', function ()
|
|
||||||
{
|
|
||||||
console.info1('Connection Bar Forcefully closed');
|
|
||||||
MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", this.state);
|
|
||||||
for (var i in this.state.desktop.kvm._pipedStreams)
|
|
||||||
{
|
|
||||||
this.state.desktop.kvm._pipedStreams[i].end();
|
|
||||||
}
|
|
||||||
this.state.desktop.kvm.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.httprequest.desktop.kvm.pipe(this, { dataTypeSkip: 1 });
|
|
||||||
if (this.httprequest.autolock)
|
|
||||||
{
|
|
||||||
destopLockHelper_pipe(this.httprequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeAllListeners('data');
|
this.removeAllListeners('data');
|
||||||
|
|
@ -3116,6 +3218,12 @@ function onTunnelData(data)
|
||||||
|
|
||||||
this.descriptorMetadata = "Remote Files";
|
this.descriptorMetadata = "Remote Files";
|
||||||
|
|
||||||
|
// Look for a TSID
|
||||||
|
var tsid = null;
|
||||||
|
if ((this.httprequest.xoptions != null) && (typeof this.httprequest.xoptions.tsid == 'number')) { tsid = this.httprequest.xoptions.tsid; }
|
||||||
|
require('MeshAgent')._tsid = tsid;
|
||||||
|
this.tsid = tsid;
|
||||||
|
|
||||||
// Add the files session to the count to update the server
|
// Add the files session to the count to update the server
|
||||||
if (this.httprequest.userid != null) {
|
if (this.httprequest.userid != null) {
|
||||||
var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest);
|
var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest);
|
||||||
|
|
@ -3138,69 +3246,31 @@ function onTunnelData(data)
|
||||||
// Perform notification if needed. Toast messages may not be supported on all platforms.
|
// Perform notification if needed. Toast messages may not be supported on all platforms.
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 32))
|
if (this.httprequest.consent && (this.httprequest.consent & 32))
|
||||||
{
|
{
|
||||||
// User Consent Prompt is required
|
// User asked for consent so now we check if we can auto accept if no user is present/loggedin
|
||||||
// Send a console message back using the console channel, "\n" is supported.
|
if (this.httprequest.consentAutoAcceptIfNoUser) {
|
||||||
this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
|
var p = require('user-sessions').enumerateUsers();
|
||||||
var consentMessage = currentTranslation['fileConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
|
p.sessionid = this.httprequest.sessionid;
|
||||||
var consentTitle = 'MeshCentral';
|
p.ws = this;
|
||||||
|
p.then(function (u) {
|
||||||
if (this.httprequest.soptions != null)
|
var v = [];
|
||||||
{
|
for (var i in u) {
|
||||||
if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
|
if (u[i].State == 'Active') { v.push({ tsid: i, type: u[i].StationName, user: u[i].Username, domain: u[i].Domain }); }
|
||||||
if (this.httprequest.soptions.consentMsgFiles != null) { consentMessage = this.httprequest.soptions.consentMsgFiles.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
|
}
|
||||||
}
|
if (v.length == 0) { // No user is present, auto accept
|
||||||
var pr;
|
// User Consent Prompt is not required
|
||||||
if (process.platform == 'win32')
|
files_consent_ok(this.ws);
|
||||||
{
|
} else {
|
||||||
var enhanced = false;
|
// User is present so we still need consent
|
||||||
try { require('win-userconsent'); enhanced = true; } catch (ex) { }
|
files_consent_ask(this.ws);
|
||||||
if (enhanced)
|
}
|
||||||
{
|
});
|
||||||
var ipr = server_getUserImage(this.httprequest.userid);
|
|
||||||
ipr.consentTitle = consentTitle;
|
|
||||||
ipr.consentMessage = consentMessage;
|
|
||||||
ipr.consentTimeout = this.httprequest.consentTimeout;
|
|
||||||
ipr.consentAutoAccept = this.httprequest.consentAutoAccept;
|
|
||||||
ipr.username = this.httprequest.realname;
|
|
||||||
ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
|
|
||||||
pr = ipr.then(function (img)
|
|
||||||
{
|
|
||||||
this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground });
|
|
||||||
this.__childPromise.close = this.consent.close.bind(this.consent);
|
|
||||||
return (this.consent);
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null);
|
|
||||||
}
|
|
||||||
pr.ws = this;
|
|
||||||
this.pause();
|
|
||||||
this._consentpromise = pr;
|
|
||||||
this.prependOnceListener('end', files_tunnel_endhandler);
|
|
||||||
pr.then(files_consentpromise_resolved, files_consentpromise_rejected);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// User Consent Prompt is not required
|
|
||||||
if (this.httprequest.consent && (this.httprequest.consent & 4)) {
|
|
||||||
// User Notifications is required
|
|
||||||
MeshServerLogEx(42, null, "Started remote files with toast notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
|
||||||
var notifyMessage = currentTranslation['fileNotify'].replace('{0}', this.httprequest.realname);
|
|
||||||
var notifyTitle = "MeshCentral";
|
|
||||||
if (this.httprequest.soptions != null) {
|
|
||||||
if (this.httprequest.soptions.notifyTitle != null) { notifyTitle = this.httprequest.soptions.notifyTitle; }
|
|
||||||
if (this.httprequest.soptions.notifyMsgFiles != null) { notifyMessage = this.httprequest.soptions.notifyMsgFiles.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
|
|
||||||
}
|
|
||||||
try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
|
|
||||||
} else {
|
} else {
|
||||||
MeshServerLogEx(43, null, "Started remote files without notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
|
// User Consent Prompt is required
|
||||||
|
files_consent_ask(this);
|
||||||
}
|
}
|
||||||
this.resume();
|
} else {
|
||||||
|
// User Consent Prompt is not required
|
||||||
|
files_consent_ok(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup files
|
// Setup files
|
||||||
|
|
@ -3688,7 +3758,14 @@ function onTunnelControlData(data, ws) {
|
||||||
{ // Desktop
|
{ // Desktop
|
||||||
// Switch the user input from websocket to webrtc at this point.
|
// Switch the user input from websocket to webrtc at this point.
|
||||||
ws.unpipe(ws.httprequest.desktop.kvm);
|
ws.unpipe(ws.httprequest.desktop.kvm);
|
||||||
try { ws.webrtc.rtcchannel.pipe(ws.httprequest.desktop.kvm, { dataTypeSkip: 1, end: false }); } catch (ex) { sendConsoleText('EX2'); } // 0 = Binary, 1 = Text.
|
if ((ws.httprequest.desktopviewonly != true) && ((ws.httprequest.rights == 0xFFFFFFFF) || (((ws.httprequest.rights & MESHRIGHT_REMOTECONTROL) != 0) && ((ws.httprequest.rights & MESHRIGHT_REMOTEVIEW) == 0)))) {
|
||||||
|
// If we have remote control rights, pipe the KVM input
|
||||||
|
try { ws.webrtc.rtcchannel.pipe(ws.httprequest.desktop.kvm, { dataTypeSkip: 1, end: false }); } catch (ex) { sendConsoleText('EX2'); } // 0 = Binary, 1 = Text.
|
||||||
|
} else {
|
||||||
|
// We need to only pipe non-mouse & non-keyboard inputs.
|
||||||
|
// sendConsoleText('Warning: No Remote Desktop Input Rights.');
|
||||||
|
// TODO!!!
|
||||||
|
}
|
||||||
ws.resume(); // Resume the websocket to keep receiving control data
|
ws.resume(); // Resume the websocket to keep receiving control data
|
||||||
}
|
}
|
||||||
ws.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc2\"}'); // Indicates we will no longer get any data on websocket, switching to WebRTC at this point.
|
ws.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc2\"}'); // Indicates we will no longer get any data on websocket, switching to WebRTC at this point.
|
||||||
|
|
@ -3874,11 +3951,11 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
|
||||||
if (require('os').dns != null) { availcommands += ',dnsinfo'; }
|
if (require('os').dns != null) { availcommands += ',dnsinfo'; }
|
||||||
try { require('linux-dhcp'); availcommands += ',dhcp'; } catch (ex) { }
|
try { require('linux-dhcp'); availcommands += ',dhcp'; } catch (ex) { }
|
||||||
if (process.platform == 'win32') {
|
if (process.platform == 'win32') {
|
||||||
availcommands += ',bitlocker,cs,wpfhwacceleration,uac,volumes,rdpport';
|
availcommands += ',bitlocker,cs,wpfhwacceleration,uac,volumes,rdpport,deskbackground';
|
||||||
if (bcdOK()) { availcommands += ',safemode'; }
|
if (bcdOK()) { availcommands += ',safemode'; }
|
||||||
if (require('notifybar-desktop').DefaultPinned != null) { availcommands += ',privacybar'; }
|
if (require('notifybar-desktop').DefaultPinned != null) { availcommands += ',privacybar'; }
|
||||||
try { require('win-utils'); availcommands += ',taskbar'; } catch (ex) { }
|
try { require('win-utils'); availcommands += ',taskbar'; } catch (ex) { }
|
||||||
try { require('win-info'); availcommands += ',installedapps'; } catch (ex) { }
|
try { require('win-info'); availcommands += ',installedapps,qfe'; } catch (ex) { }
|
||||||
}
|
}
|
||||||
if (amt != null) { availcommands += ',amt,amtconfig,amtevents'; }
|
if (amt != null) { availcommands += ',amt,amtconfig,amtevents'; }
|
||||||
if (process.platform != 'freebsd') { availcommands += ',vm'; }
|
if (process.platform != 'freebsd') { availcommands += ',vm'; }
|
||||||
|
|
@ -4037,12 +4114,9 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
|
||||||
break;
|
break;
|
||||||
case 'bitlocker':
|
case 'bitlocker':
|
||||||
if (process.platform == 'win32') {
|
if (process.platform == 'win32') {
|
||||||
if (require('computer-identifiers').volumes_promise != null) {
|
if (require('win-volumes').volumes_promise != null) {
|
||||||
var p = require('computer-identifiers').volumes_promise();
|
var p = require('win-volumes').volumes_promise();
|
||||||
p.then(function (res) { sendConsoleText(JSON.stringify(cleanGetBitLockerVolumeInfo(res), null, 1), this.session); });
|
p.then(function (res) { sendConsoleText(JSON.stringify(cleanGetBitLockerVolumeInfo(res), null, 1), this.session); });
|
||||||
response = "Please wait...";
|
|
||||||
} else if (require('computer-identifiers').volumes != null) {
|
|
||||||
sendConsoleText(JSON.stringify(cleanGetBitLockerVolumeInfo(require('computer-identifiers').volumes()), null, 1), this.session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -4292,7 +4366,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
|
||||||
}
|
}
|
||||||
case 'agentmsg': {
|
case 'agentmsg': {
|
||||||
if (args['_'].length == 0) {
|
if (args['_'].length == 0) {
|
||||||
response = "Proper usage:\r\n agentmsg add \"[message]\" [iconIndex]\r\n agentmsg remove [index]\r\n agentmsg list"; // Display usage
|
response = "Proper usage:\r\n agentmsg add \"[message]\" [iconIndex]\r\n agentmsg remove [id]\r\n agentmsg list"; // Display usage
|
||||||
} else {
|
} else {
|
||||||
if ((args['_'][0] == 'add') && (args['_'].length > 1)) {
|
if ((args['_'][0] == 'add') && (args['_'].length > 1)) {
|
||||||
var msgID, iconIndex = 0;
|
var msgID, iconIndex = 0;
|
||||||
|
|
@ -4510,10 +4584,11 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
|
||||||
if (process.platform == 'win32') {
|
if (process.platform == 'win32') {
|
||||||
// Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value
|
// Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value
|
||||||
var writtenSize = 0;
|
var writtenSize = 0;
|
||||||
try { writtenSize = require('win-registry').QueryKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MeshCentralAgent', 'EstimatedSize'); } catch (ex) { response = ex; }
|
var serviceName = (_MSH().serviceName ? _MSH().serviceName : (require('_agentNodeId').serviceName() ? require('_agentNodeId').serviceName() : 'Mesh Agent'));
|
||||||
|
try { writtenSize = require('win-registry').QueryKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'EstimatedSize'); } catch (ex) { response = ex; }
|
||||||
if (writtenSize != actualSize) {
|
if (writtenSize != actualSize) {
|
||||||
response = "Size updated from: " + writtenSize + " to: " + actualSize;
|
response = "Size updated from: " + writtenSize + " to: " + actualSize;
|
||||||
try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MeshCentralAgent', 'EstimatedSize', actualSize); } catch (ex) { response = ex; }
|
try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + serviceName, 'EstimatedSize', actualSize); } catch (ex) { response = ex; }
|
||||||
} else
|
} else
|
||||||
{ response = "Agent Size: " + actualSize + " kb"; }
|
{ response = "Agent Size: " + actualSize + " kb"; }
|
||||||
} else
|
} else
|
||||||
|
|
@ -5322,6 +5397,13 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'qfe': {
|
||||||
|
if(process.platform == 'win32'){
|
||||||
|
var qfe = require('win-info').qfe();
|
||||||
|
sendConsoleText(JSON.stringify(qfe,null,1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: { // This is an unknown command, return an error message
|
default: { // This is an unknown command, return an error message
|
||||||
response = "Unknown command \"" + cmd + "\", type \"help\" for list of available commands.";
|
response = "Unknown command \"" + cmd + "\", type \"help\" for list of available commands.";
|
||||||
break;
|
break;
|
||||||
|
|
@ -5590,8 +5672,8 @@ function windows_execve(name, agentfilename, sessionid) {
|
||||||
var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true });
|
var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true });
|
||||||
var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize);
|
var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize);
|
||||||
var arg1 = require('_GenericMarshal').CreateVariable('cmd.exe', { wide: true });
|
var arg1 = require('_GenericMarshal').CreateVariable('cmd.exe', { wide: true });
|
||||||
var arg2 = require('_GenericMarshal').CreateVariable('/C wmic service "' + name + '" call stopservice & "' + process.cwd() + agentfilename + '.update.exe" -b64exec ' + 'dHJ5CnsKICAgIHZhciBzZXJ2aWNlTG9jYXRpb24gPSBwcm9jZXNzLmFyZ3YucG9wKCkudG9Mb3dlckNhc2UoKTsKICAgIHJlcXVpcmUoJ3Byb2Nlc3MtbWFuYWdlcicpLmVudW1lcmF0ZVByb2Nlc3NlcygpLnRoZW4oZnVuY3Rpb24gKHByb2MpCiAgICB7CiAgICAgICAgZm9yICh2YXIgcCBpbiBwcm9jKQogICAgICAgIHsKICAgICAgICAgICAgaWYgKHByb2NbcF0ucGF0aCAmJiAocHJvY1twXS5wYXRoLnRvTG93ZXJDYXNlKCkgPT0gc2VydmljZUxvY2F0aW9uKSkKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvY2Vzcy5raWxsKHByb2NbcF0ucGlkKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwcm9jZXNzLmV4aXQoKTsKICAgIH0pOwp9CmNhdGNoIChlKQp7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQ==' +
|
var arg2 = require('_GenericMarshal').CreateVariable('/C net stop "' + name + '" & "' + process.cwd() + agentfilename + '.update.exe" -b64exec ' + 'dHJ5CnsKICAgIHZhciBzZXJ2aWNlTG9jYXRpb24gPSBwcm9jZXNzLmFyZ3YucG9wKCkudG9Mb3dlckNhc2UoKTsKICAgIHJlcXVpcmUoJ3Byb2Nlc3MtbWFuYWdlcicpLmVudW1lcmF0ZVByb2Nlc3NlcygpLnRoZW4oZnVuY3Rpb24gKHByb2MpCiAgICB7CiAgICAgICAgZm9yICh2YXIgcCBpbiBwcm9jKQogICAgICAgIHsKICAgICAgICAgICAgaWYgKHByb2NbcF0ucGF0aCAmJiAocHJvY1twXS5wYXRoLnRvTG93ZXJDYXNlKCkgPT0gc2VydmljZUxvY2F0aW9uKSkKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvY2Vzcy5raWxsKHByb2NbcF0ucGlkKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwcm9jZXNzLmV4aXQoKTsKICAgIH0pOwp9CmNhdGNoIChlKQp7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQ==' +
|
||||||
' "' + process.execPath + '" & copy "' + process.cwd() + agentfilename + '.update.exe" "' + process.execPath + '" & wmic service "' + name + '" call startservice & erase "' + process.cwd() + agentfilename + '.update.exe"', { wide: true });
|
' "' + process.execPath + '" & copy "' + process.cwd() + agentfilename + '.update.exe" "' + process.execPath + '" & net start "' + name + '" & erase "' + process.cwd() + agentfilename + '.update.exe"', { wide: true });
|
||||||
|
|
||||||
arg1.pointerBuffer().copy(args.toBuffer());
|
arg1.pointerBuffer().copy(args.toBuffer());
|
||||||
arg2.pointerBuffer().copy(args.toBuffer(), require('_GenericMarshal').PointerSize);
|
arg2.pointerBuffer().copy(args.toBuffer(), require('_GenericMarshal').PointerSize);
|
||||||
|
|
@ -5781,6 +5863,7 @@ function handleServerConnection(state) {
|
||||||
|
|
||||||
// Update the server on with basic info, logged in users and more advanced stuff, like Intel ME and Network Settings
|
// Update the server on with basic info, logged in users and more advanced stuff, like Intel ME and Network Settings
|
||||||
meInfoStr = null;
|
meInfoStr = null;
|
||||||
|
LastPeriodicServerUpdate = null;
|
||||||
sendPeriodicServerUpdate(null, true);
|
sendPeriodicServerUpdate(null, true);
|
||||||
if (selfInfoUpdateTimer == null) {
|
if (selfInfoUpdateTimer == null) {
|
||||||
selfInfoUpdateTimer = setInterval(sendPeriodicServerUpdate, 1200000); // 20 minutes
|
selfInfoUpdateTimer = setInterval(sendPeriodicServerUpdate, 1200000); // 20 minutes
|
||||||
|
|
|
||||||
|
|
@ -225,19 +225,14 @@ function macos_memUtilization()
|
||||||
function windows_thermals()
|
function windows_thermals()
|
||||||
{
|
{
|
||||||
var ret = [];
|
var ret = [];
|
||||||
child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', '/namespace:\\\\root\\wmi', 'PATH', 'MSAcpi_ThermalZoneTemperature', 'get', 'CurrentTemperature']);
|
try {
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']);
|
||||||
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
if (ret[0]) {
|
||||||
child.waitExit();
|
for (var i = 0; i < ret.length; ++i) {
|
||||||
|
ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2);
|
||||||
if(child.stdout.str.trim!='')
|
}
|
||||||
{
|
|
||||||
var lines = child.stdout.str.trim().split('\r\n');
|
|
||||||
for (var i = 1; i < lines.length; ++i)
|
|
||||||
{
|
|
||||||
if (lines[i].trim() != '') { ret.push(((parseFloat(lines[i]) / 10) - 273.15).toFixed(2)); }
|
|
||||||
}
|
}
|
||||||
}
|
} catch (ex) { }
|
||||||
return (ret);
|
return (ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,16 +280,10 @@ function macos_thermals()
|
||||||
return (ret);
|
return (ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(process.platform)
|
const platformConfig = {
|
||||||
{
|
linux: { cpuUtilization: linux_cpuUtilization, memUtilization: linux_memUtilization, thermals: linux_thermals },
|
||||||
case 'linux':
|
win32: { cpuUtilization: windows_cpuUtilization, memUtilization: windows_memUtilization, thermals: windows_thermals },
|
||||||
module.exports = { cpuUtilization: linux_cpuUtilization, memUtilization: linux_memUtilization, thermals: linux_thermals };
|
darwin: { cpuUtilization: macos_cpuUtilization, memUtilization: macos_memUtilization, thermals: macos_thermals }
|
||||||
break;
|
};
|
||||||
case 'win32':
|
|
||||||
module.exports = { cpuUtilization: windows_cpuUtilization, memUtilization: windows_memUtilization, thermals: windows_thermals };
|
|
||||||
break;
|
|
||||||
case 'darwin':
|
|
||||||
module.exports = { cpuUtilization: macos_cpuUtilization, memUtilization: macos_memUtilization, thermals: macos_thermals };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
module.exports = platformConfig[process.platform];
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,19 @@ function linux_identifiers()
|
||||||
identifiers['board_vendor'] = 'Raspberry Pi';
|
identifiers['board_vendor'] = 'Raspberry Pi';
|
||||||
identifiers['board_name'] = require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim();
|
identifiers['board_name'] = require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim();
|
||||||
identifiers['board_serial'] = require('fs').readFileSync('/sys/firmware/devicetree/base/serial-number').toString().trim();
|
identifiers['board_serial'] = require('fs').readFileSync('/sys/firmware/devicetree/base/serial-number').toString().trim();
|
||||||
|
const memorySlots = [];
|
||||||
|
var child = require('child_process').execFile('/bin/sh', ['sh']);
|
||||||
|
child.stdout.str = ''; child.stdout.on('data', dataHandler);
|
||||||
|
child.stdin.write('vcgencmd get_mem arm && vcgencmd get_mem gpu\nexit\n');
|
||||||
|
child.waitExit();
|
||||||
|
try {
|
||||||
|
const lines = child.stdout.str.trim().split('\n');
|
||||||
|
if (lines.length == 2) {
|
||||||
|
memorySlots.push({ Locator: "ARM Memory", Size: lines[0].split('=')[1].trim() })
|
||||||
|
memorySlots.push({ Locator: "GPU Memory", Size: lines[1].split('=')[1].trim() })
|
||||||
|
ret.memory = { Memory_Device: memorySlots };
|
||||||
|
}
|
||||||
|
} catch (xx) { }
|
||||||
} else {
|
} else {
|
||||||
throw('Unknown board');
|
throw('Unknown board');
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +156,7 @@ function linux_identifiers()
|
||||||
// Fetch storage volumes using df
|
// Fetch storage volumes using df
|
||||||
child = require('child_process').execFile('/bin/sh', ['sh']);
|
child = require('child_process').execFile('/bin/sh', ['sh']);
|
||||||
child.stdout.str = ''; child.stdout.on('data', dataHandler);
|
child.stdout.str = ''; child.stdout.on('data', dataHandler);
|
||||||
child.stdin.write('df --output=size,used,avail,target,fstype | awk \'NR>1 {printf "{\\"size\\":\\"%s\\",\\"used\\":\\"%s\\",\\"available\\":\\"%s\\",\\"mount_point\\":\\"%s\\",\\"type\\":\\"%s\\"},", $1, $2, $3, $4, $5}\' | sed \'$ s/,$//\' | awk \'BEGIN {printf "["} {printf "%s", $0} END {printf "]"}\'\nexit\n');
|
child.stdin.write('df -T | awk \'NR==1 || $1 ~ ".+"{print $3, $4, $5, $7, $2}\' | awk \'NR>1 {printf "{\\"size\\":\\"%s\\",\\"used\\":\\"%s\\",\\"available\\":\\"%s\\",\\"mount_point\\":\\"%s\\",\\"type\\":\\"%s\\"},", $1, $2, $3, $4, $5}\' | sed \'$ s/,$//\' | awk \'BEGIN {printf "["} {printf "%s", $0} END {printf "]"}\'\nexit\n');
|
||||||
child.waitExit();
|
child.waitExit();
|
||||||
try { ret.volumes = JSON.parse(child.stdout.str.trim()); } catch (xx) { }
|
try { ret.volumes = JSON.parse(child.stdout.str.trim()); } catch (xx) { }
|
||||||
child = null;
|
child = null;
|
||||||
|
|
@ -351,7 +364,18 @@ function linux_identifiers()
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
||||||
child.stderr.on('data', function () { });
|
child.stderr.on('data', function () { });
|
||||||
child.waitExit();
|
child.waitExit();
|
||||||
values.linux.LastBootUpTime = child.stdout.str.trim();
|
var regex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
||||||
|
if (regex.test(child.stdout.str.trim())) {
|
||||||
|
values.linux.LastBootUpTime = child.stdout.str.trim();
|
||||||
|
} else {
|
||||||
|
child = require('child_process').execFile('/bin/sh', ['sh']);
|
||||||
|
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
||||||
|
child.stdin.write('date -d "@$(( $(date +%s) - $(awk \'{print int($1)}\' /proc/uptime) ))" "+%Y-%m-%d %H:%M:%S"\nexit\n');
|
||||||
|
child.waitExit();
|
||||||
|
if (regex.test(child.stdout.str.trim())) {
|
||||||
|
values.linux.LastBootUpTime = child.stdout.str.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
child = null;
|
child = null;
|
||||||
} catch (ex) { }
|
} catch (ex) { }
|
||||||
|
|
||||||
|
|
@ -398,104 +422,6 @@ function windows_wmic_results(str)
|
||||||
return (result);
|
return (result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function windows_volumes()
|
|
||||||
{
|
|
||||||
var promise = require('promise');
|
|
||||||
var p1 = new promise(function (res, rej) { this._res = res; this._rej = rej; });
|
|
||||||
var p2 = new promise(function (res, rej) { this._res = res; this._rej = rej; });
|
|
||||||
|
|
||||||
p1._p2 = p2;
|
|
||||||
p2._p1 = p1;
|
|
||||||
|
|
||||||
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', ['powershell', '-noprofile', '-nologo', '-command', '-']);
|
|
||||||
p1.child = child;
|
|
||||||
child.promise = p1;
|
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
|
||||||
child.stdin.write('Get-Volume | Select-Object -Property DriveLetter,FileSystemLabel,FileSystemType,Size,SizeRemaining,DriveType | ConvertTo-Csv -NoTypeInformation\r\nexit\r\n');
|
|
||||||
child.on('exit', function (c)
|
|
||||||
{
|
|
||||||
var a, i, tokens, key;
|
|
||||||
var ret = {};
|
|
||||||
|
|
||||||
a = this.stdout.str.trim().split('\r\n');
|
|
||||||
for (i = 1; i < a.length; ++i)
|
|
||||||
{
|
|
||||||
tokens = a[i].split(',');
|
|
||||||
if (tokens[0] != '' && tokens[1] != undefined)
|
|
||||||
{
|
|
||||||
ret[tokens[0].split('"')[1]] =
|
|
||||||
{
|
|
||||||
name: tokens[1].split('"')[1],
|
|
||||||
type: tokens[2].split('"')[1],
|
|
||||||
size: tokens[3].split('"')[1],
|
|
||||||
sizeremaining: tokens[4].split('"')[1],
|
|
||||||
removable: tokens[5].split('"')[1] == 'Removable',
|
|
||||||
cdrom: tokens[5].split('"')[1] == 'CD-ROM'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.promise._res({ r: ret, t: tokens });
|
|
||||||
});
|
|
||||||
|
|
||||||
p1.then(function (j)
|
|
||||||
{
|
|
||||||
var ret = j.r;
|
|
||||||
var tokens = j.t;
|
|
||||||
|
|
||||||
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', ['powershell', '-noprofile', '-nologo', '-command', '-']);
|
|
||||||
p2.child = child;
|
|
||||||
child.promise = p2;
|
|
||||||
child.tokens = tokens;
|
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
|
||||||
child.stdin.write('Get-BitLockerVolume | Select-Object -Property MountPoint,VolumeStatus,ProtectionStatus | ConvertTo-Csv -NoTypeInformation\r\nexit\r\n');
|
|
||||||
child.on('exit', function ()
|
|
||||||
{
|
|
||||||
var i;
|
|
||||||
var a = this.stdout.str.trim().split('\r\n');
|
|
||||||
for (i = 1; i < a.length; ++i)
|
|
||||||
{
|
|
||||||
tokens = a[i].split(',');
|
|
||||||
key = tokens[0].split(':').shift().split('"').pop();
|
|
||||||
if (ret[key] != null)
|
|
||||||
{
|
|
||||||
ret[key].volumeStatus = tokens[1].split('"')[1];
|
|
||||||
ret[key].protectionStatus = tokens[2].split('"')[1];
|
|
||||||
try {
|
|
||||||
var foundIDMarkedLine = false, foundMarkedLine = false, identifier = '', password = '';
|
|
||||||
var keychild = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'manage-bde -protectors -get ', tokens[0].split('"')[1], ' -Type recoverypassword'], {});
|
|
||||||
keychild.stdout.str = ''; keychild.stdout.on('data', function (c) { this.str += c.toString(); });
|
|
||||||
keychild.waitExit();
|
|
||||||
var lines = keychild.stdout.str.trim().split('\r\n');
|
|
||||||
for (var x = 0; x < lines.length; x++) { // Loop each line
|
|
||||||
var abc = lines[x].trim();
|
|
||||||
var englishidpass = (abc !== '' && abc.includes('Numerical Password:')); // English ID
|
|
||||||
var germanidpass = (abc !== '' && abc.includes('Numerisches Kennwort:')); // German ID
|
|
||||||
var frenchidpass = (abc !== '' && abc.includes('Mot de passe num')); // French ID
|
|
||||||
var englishpass = (abc !== '' && abc.includes('Password:') && !abc.includes('Numerical Password:')); // English Password
|
|
||||||
var germanpass = (abc !== '' && abc.includes('Kennwort:') && !abc.includes('Numerisches Kennwort:')); // German Password
|
|
||||||
var frenchpass = (abc !== '' && abc.includes('Mot de passe :') && !abc.includes('Mot de passe num')); // French Password
|
|
||||||
if (englishidpass || germanidpass || frenchidpass|| englishpass || germanpass || frenchpass) {
|
|
||||||
var nextline = lines[x + 1].trim();
|
|
||||||
if (x + 1 < lines.length && (nextline !== '' && (nextline.startsWith('ID:') || nextline.startsWith('ID :')) )) {
|
|
||||||
identifier = nextline.replace('ID:','').replace('ID :', '').trim();
|
|
||||||
foundIDMarkedLine = true;
|
|
||||||
}else if (x + 1 < lines.length && nextline !== '') {
|
|
||||||
password = nextline;
|
|
||||||
foundMarkedLine = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret[key].identifier = (foundIDMarkedLine ? identifier : ''); // Set Bitlocker Identifier
|
|
||||||
ret[key].recoveryPassword = (foundMarkedLine ? password : ''); // Set Bitlocker Password
|
|
||||||
} catch(ex) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.promise._res(ret);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return (p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function windows_identifiers()
|
function windows_identifiers()
|
||||||
{
|
{
|
||||||
var ret = { windows: {} };
|
var ret = { windows: {} };
|
||||||
|
|
@ -776,32 +702,35 @@ function hexToAscii(hexString) {
|
||||||
|
|
||||||
function win_chassisType()
|
function win_chassisType()
|
||||||
{
|
{
|
||||||
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', 'SystemEnclosure', 'get', 'ChassisTypes']);
|
// needs to be replaced with win-wmi but due to bug in win-wmi it doesnt handle arrays correctly
|
||||||
|
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', ['powershell', '-noprofile', '-nologo', '-command', '-'], {});
|
||||||
|
if (child == null) { return ([]); }
|
||||||
|
child.descriptorMetadata = 'process-manager';
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
||||||
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
||||||
|
child.stdin.write('Get-WmiObject Win32_SystemEnclosure | Select-Object -ExpandProperty ChassisTypes\r\n');
|
||||||
|
child.stdin.write('exit\r\n');
|
||||||
child.waitExit();
|
child.waitExit();
|
||||||
|
try {
|
||||||
try
|
return (parseInt(child.stdout.str));
|
||||||
{
|
} catch (e) {
|
||||||
var tok = child.stdout.str.split('{')[1].split('}')[0];
|
|
||||||
var val = tok.split(',')[0];
|
|
||||||
return (parseInt(val));
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
return (2); // unknown
|
return (2); // unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function win_systemType()
|
function win_systemType()
|
||||||
{
|
{
|
||||||
var CSV = '/FORMAT:"' + require('util-language').wmicXslPath + 'csv"';
|
try {
|
||||||
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', 'ComputerSystem', 'get', 'PCSystemType', CSV]);
|
var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']);
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
if (tokens[0]) {
|
||||||
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
return (parseInt(tokens[0]['PCSystemType']));
|
||||||
child.waitExit();
|
} else {
|
||||||
|
return (parseInt(1)); // default is desktop
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
return (parseInt(1)); // default is desktop
|
||||||
|
}
|
||||||
|
|
||||||
return (parseInt(child.stdout.str.trim().split(',').pop()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function win_formFactor(chassistype)
|
function win_formFactor(chassistype)
|
||||||
|
|
@ -964,11 +893,6 @@ module.exports.isVM = function isVM()
|
||||||
return (ret);
|
return (ret);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.platform == 'win32')
|
|
||||||
{
|
|
||||||
module.exports.volumes_promise = windows_volumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bios_date = BIOS->ReleaseDate
|
// bios_date = BIOS->ReleaseDate
|
||||||
// bios_vendor = BIOS->Manufacturer
|
// bios_vendor = BIOS->Manufacturer
|
||||||
// bios_version = BIOS->SMBIOSBIOSVersion
|
// bios_version = BIOS->SMBIOSBIOSVersion
|
||||||
|
|
|
||||||
|
|
@ -229,25 +229,14 @@ function macos_memUtilization()
|
||||||
function windows_thermals()
|
function windows_thermals()
|
||||||
{
|
{
|
||||||
var ret = [];
|
var ret = [];
|
||||||
child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', '/namespace:\\\\root\\wmi', 'PATH', 'MSAcpi_ThermalZoneTemperature', 'get', 'CurrentTemperature,InstanceName', '/FORMAT:CSV']);
|
try {
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']);
|
||||||
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
if (ret[0]) {
|
||||||
child.waitExit();
|
for (var i = 0; i < ret.length; ++i) {
|
||||||
if(child.stdout.str.trim()!='')
|
ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2);
|
||||||
{
|
|
||||||
var lines = child.stdout.str.trim().split('\r\n');
|
|
||||||
var keys = lines[0].trim().split(',');
|
|
||||||
for (var i = 1; i < lines.length; ++i)
|
|
||||||
{
|
|
||||||
var obj = {};
|
|
||||||
var tokens = lines[i].trim().split(',');
|
|
||||||
for (var key = 0; key < keys.length; ++key)
|
|
||||||
{
|
|
||||||
if (tokens[key]) { obj[keys[key]] = key==1 ? ((parseFloat(tokens[key]) / 10) - 273.15).toFixed(2) : tokens[key]; }
|
|
||||||
}
|
}
|
||||||
ret.push(obj);
|
|
||||||
}
|
}
|
||||||
}
|
} catch (ex) { }
|
||||||
return (ret);
|
return (ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,28 +18,21 @@ var promise = require('promise');
|
||||||
|
|
||||||
function qfe()
|
function qfe()
|
||||||
{
|
{
|
||||||
var child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', 'qfe', 'list', 'full', '/FORMAT:CSV']);
|
try {
|
||||||
child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); });
|
var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering');
|
||||||
child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); });
|
if (tokens[0]){
|
||||||
child.waitExit();
|
for (var index = 0; index < tokens.length; index++) {
|
||||||
|
for (var key in tokens[index]) {
|
||||||
var lines = child.stdout.str.trim().split('\r\n');
|
if (key.startsWith('__')) delete tokens[index][key];
|
||||||
var keys = lines[0].split(',');
|
}
|
||||||
var i, key;
|
}
|
||||||
var tokens;
|
return (tokens);
|
||||||
var result = [];
|
} else {
|
||||||
|
return ([]);
|
||||||
for (i = 1; i < lines.length; ++i)
|
|
||||||
{
|
|
||||||
var obj = {};
|
|
||||||
tokens = lines[i].split(',');
|
|
||||||
for (key = 0; key < keys.length; ++key)
|
|
||||||
{
|
|
||||||
if (tokens[key]) { obj[keys[key]] = tokens[key]; }
|
|
||||||
}
|
}
|
||||||
result.push(obj);
|
} catch (ex) {
|
||||||
|
return ([]);
|
||||||
}
|
}
|
||||||
return (result);
|
|
||||||
}
|
}
|
||||||
function av()
|
function av()
|
||||||
{
|
{
|
||||||
|
|
@ -258,8 +251,12 @@ function defender(){
|
||||||
ret.child.stdin.write('exit\r\n');
|
ret.child.stdin.write('exit\r\n');
|
||||||
ret.child.on('exit', function (c) {
|
ret.child.on('exit', function (c) {
|
||||||
if (this.stdout.str == '') { this.promise._resolve({}); return; }
|
if (this.stdout.str == '') { this.promise._resolve({}); return; }
|
||||||
var abc = JSON.parse(this.stdout.str.trim())
|
try {
|
||||||
this.promise._resolve({ RealTimeProtection: abc.RealTimeProtectionEnabled, TamperProtected: abc.IsTamperProtected });
|
var abc = JSON.parse(this.stdout.str.trim());
|
||||||
|
this.promise._resolve({ RealTimeProtection: abc.RealTimeProtectionEnabled, TamperProtected: abc.IsTamperProtected });
|
||||||
|
} catch (ex) {
|
||||||
|
this.promise._resolve({}); return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return (ret);
|
return (ret);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,90 @@ function getVolumes()
|
||||||
{
|
{
|
||||||
ret[v[i].DeviceID] = trimObject(v[i]);
|
ret[v[i].DeviceID] = trimObject(v[i]);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume');
|
v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume');
|
||||||
for (i in v)
|
for (i in v)
|
||||||
{
|
|
||||||
var tmp = trimObject(v[i]);
|
|
||||||
for (var k in tmp)
|
|
||||||
{
|
{
|
||||||
ret[tmp.DeviceID][k] = tmp[k];
|
var tmp = trimObject(v[i]);
|
||||||
|
for (var k in tmp)
|
||||||
|
{
|
||||||
|
ret[tmp.DeviceID][k] = tmp[k];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (ex) { }
|
||||||
return (ret);
|
return (ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getVolumes: function () { try { return (getVolumes()); } catch (x) { return ({}); } } };
|
function windows_volumes()
|
||||||
|
{
|
||||||
|
var promise = require('promise');
|
||||||
|
var p1 = new promise(function (res, rej) { this._res = res; this._rej = rej; });
|
||||||
|
var ret = {};
|
||||||
|
var values = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_LogicalDisk', ['DeviceID', 'VolumeName', 'FileSystem', 'Size', 'FreeSpace', 'DriveType']);
|
||||||
|
if(values[0]){
|
||||||
|
for (var i = 0; i < values.length; ++i) {
|
||||||
|
var drive = values[i]['DeviceID'].slice(0,-1);
|
||||||
|
ret[drive] = {
|
||||||
|
name: (values[i]['VolumeName'] ? values[i]['VolumeName'] : ""),
|
||||||
|
type: (values[i]['FileSystem'] ? values[i]['FileSystem'] : "Unknown"),
|
||||||
|
size: (values[i]['Size'] ? values[i]['Size'] : 0),
|
||||||
|
sizeremaining: (values[i]['FreeSpace'] ? values[i]['FreeSpace'] : 0),
|
||||||
|
removable: (values[i]['DriveType'] == 2),
|
||||||
|
cdrom: (values[i]['DriveType'] == 5)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
values = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume', ['DriveLetter','ConversionStatus','ProtectionStatus']);
|
||||||
|
if(values[0]){
|
||||||
|
for (var i = 0; i < values.length; ++i) {
|
||||||
|
var drive = values[i]['DriveLetter'].slice(0,-1);
|
||||||
|
var statuses = {
|
||||||
|
0: 'FullyDecrypted',
|
||||||
|
1: 'FullyEncrypted',
|
||||||
|
2: 'EncryptionInProgress',
|
||||||
|
3: 'DecryptionInProgress',
|
||||||
|
4: 'EncryptionPaused',
|
||||||
|
5: 'DecryptionPaused'
|
||||||
|
};
|
||||||
|
ret[drive].volumeStatus = statuses.hasOwnProperty(values[i].ConversionStatus) ? statuses[values[i].ConversionStatus] : 'FullyDecrypted';
|
||||||
|
ret[drive].protectionStatus = (values[i].ProtectionStatus == 0 ? 'Off' : (values[i].ProtectionStatus == 1 ? 'On' : 'Unknown'));
|
||||||
|
try {
|
||||||
|
var foundIDMarkedLine = false, foundMarkedLine = false, identifier = '', password = '';
|
||||||
|
var keychild = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'manage-bde -protectors -get ' + drive + ': -Type recoverypassword'], {});
|
||||||
|
keychild.stdout.str = ''; keychild.stdout.on('data', function (c) { this.str += c.toString(); });
|
||||||
|
keychild.waitExit();
|
||||||
|
var lines = keychild.stdout.str.trim().split('\r\n');
|
||||||
|
for (var x = 0; x < lines.length; x++) { // Loop each line
|
||||||
|
var abc = lines[x].trim();
|
||||||
|
var englishidpass = (abc !== '' && abc.includes('Numerical Password:')); // English ID
|
||||||
|
var germanidpass = (abc !== '' && abc.includes('Numerisches Kennwort:')); // German ID
|
||||||
|
var frenchidpass = (abc !== '' && abc.includes('Mot de passe num')); // French ID
|
||||||
|
var englishpass = (abc !== '' && abc.includes('Password:') && !abc.includes('Numerical Password:')); // English Password
|
||||||
|
var germanpass = (abc !== '' && abc.includes('Kennwort:') && !abc.includes('Numerisches Kennwort:')); // German Password
|
||||||
|
var frenchpass = (abc !== '' && abc.includes('Mot de passe :') && !abc.includes('Mot de passe num')); // French Password
|
||||||
|
if (englishidpass || germanidpass || frenchidpass|| englishpass || germanpass || frenchpass) {
|
||||||
|
var nextline = lines[x + 1].trim();
|
||||||
|
if (x + 1 < lines.length && (nextline !== '' && (nextline.startsWith('ID:') || nextline.startsWith('ID :')) )) {
|
||||||
|
identifier = nextline.replace('ID:','').replace('ID :', '').trim();
|
||||||
|
foundIDMarkedLine = true;
|
||||||
|
}else if (x + 1 < lines.length && nextline !== '') {
|
||||||
|
password = nextline;
|
||||||
|
foundMarkedLine = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret[drive].identifier = (foundIDMarkedLine ? identifier : ''); // Set Bitlocker Identifier
|
||||||
|
ret[drive].recoveryPassword = (foundMarkedLine ? password : ''); // Set Bitlocker Password
|
||||||
|
} catch(ex) { } // just carry on as we cant get bitlocker key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p1._res(ret);
|
||||||
|
} catch (ex) { p1._res(ret); } // just return volumes as cant get encryption/bitlocker
|
||||||
|
return (p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getVolumes: function () { try { return (getVolumes()); } catch (x) { return ({}); } },
|
||||||
|
volumes_promise: windows_volumes
|
||||||
|
};
|
||||||
|
|
@ -485,8 +485,8 @@ function windows_execve(name, agentfilename, sessionid) {
|
||||||
var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true });
|
var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true });
|
||||||
var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize);
|
var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize);
|
||||||
var arg1 = require('_GenericMarshal').CreateVariable('cmd.exe', { wide: true });
|
var arg1 = require('_GenericMarshal').CreateVariable('cmd.exe', { wide: true });
|
||||||
var arg2 = require('_GenericMarshal').CreateVariable('/C wmic service "' + name + '" call stopservice & "' + cwd + agentfilename + '.update.exe" -b64exec ' + 'dHJ5CnsKICAgIHZhciBzZXJ2aWNlTG9jYXRpb24gPSBwcm9jZXNzLmFyZ3YucG9wKCkudG9Mb3dlckNhc2UoKTsKICAgIHJlcXVpcmUoJ3Byb2Nlc3MtbWFuYWdlcicpLmVudW1lcmF0ZVByb2Nlc3NlcygpLnRoZW4oZnVuY3Rpb24gKHByb2MpCiAgICB7CiAgICAgICAgZm9yICh2YXIgcCBpbiBwcm9jKQogICAgICAgIHsKICAgICAgICAgICAgaWYgKHByb2NbcF0ucGF0aCAmJiAocHJvY1twXS5wYXRoLnRvTG93ZXJDYXNlKCkgPT0gc2VydmljZUxvY2F0aW9uKSkKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvY2Vzcy5raWxsKHByb2NbcF0ucGlkKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwcm9jZXNzLmV4aXQoKTsKICAgIH0pOwp9CmNhdGNoIChlKQp7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQ==' +
|
var arg2 = require('_GenericMarshal').CreateVariable('/C net stop "' + name + '" & "' + cwd + agentfilename + '.update.exe" -b64exec ' + 'dHJ5CnsKICAgIHZhciBzZXJ2aWNlTG9jYXRpb24gPSBwcm9jZXNzLmFyZ3YucG9wKCkudG9Mb3dlckNhc2UoKTsKICAgIHJlcXVpcmUoJ3Byb2Nlc3MtbWFuYWdlcicpLmVudW1lcmF0ZVByb2Nlc3NlcygpLnRoZW4oZnVuY3Rpb24gKHByb2MpCiAgICB7CiAgICAgICAgZm9yICh2YXIgcCBpbiBwcm9jKQogICAgICAgIHsKICAgICAgICAgICAgaWYgKHByb2NbcF0ucGF0aCAmJiAocHJvY1twXS5wYXRoLnRvTG93ZXJDYXNlKCkgPT0gc2VydmljZUxvY2F0aW9uKSkKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvY2Vzcy5raWxsKHByb2NbcF0ucGlkKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwcm9jZXNzLmV4aXQoKTsKICAgIH0pOwp9CmNhdGNoIChlKQp7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQ==' +
|
||||||
' "' + process.execPath + '" & copy "' + cwd + agentfilename + '.update.exe" "' + process.execPath + '" & wmic service "' + name + '" call startservice & erase "' + cwd + agentfilename + '.update.exe"', { wide: true });
|
' "' + process.execPath + '" & copy "' + cwd + agentfilename + '.update.exe" "' + process.execPath + '" & net start "' + name + '" & erase "' + cwd + agentfilename + '.update.exe"', { wide: true });
|
||||||
|
|
||||||
if (name == null)
|
if (name == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -719,7 +719,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) {
|
||||||
}
|
}
|
||||||
else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i.trim(), header[i]); } // Set the headers if not blocked
|
else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i.trim(), header[i]); } // Set the headers if not blocked
|
||||||
}
|
}
|
||||||
obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future
|
// Dont set any Content-Security-Policy at all because some applications like Node-Red, access external websites from there javascript which would be forbidden by the below CSP
|
||||||
|
//obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future
|
||||||
//obj.res.set('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';"); // Set an "allow all" policy, see if the can restrict this in the future
|
//obj.res.set('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';"); // Set an "allow all" policy, see if the can restrict this in the future
|
||||||
obj.res.set('Cache-Control', 'no-store'); // Tell the browser not to cache the responses since since the relay port can be used for many relays
|
obj.res.set('Cache-Control', 'no-store'); // Tell the browser not to cache the responses since since the relay port can be used for many relays
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1566,7 +1566,11 @@ function createAuthenticodeHandler(path) {
|
||||||
options.protocol = timeServerUrl.protocol;
|
options.protocol = timeServerUrl.protocol;
|
||||||
options.hostname = timeServerUrl.hostname;
|
options.hostname = timeServerUrl.hostname;
|
||||||
options.path = timeServerUrl.pathname;
|
options.path = timeServerUrl.pathname;
|
||||||
options.port = ((timeServerUrl.port == '') ? 80 : parseInt(timeServerUrl.port));
|
let http = require("http")
|
||||||
|
if (options.protocol === "https:"){
|
||||||
|
http = require("https")
|
||||||
|
}
|
||||||
|
options.port = ((timeServerUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(timeServerUrl.port));
|
||||||
|
|
||||||
if (options.proxy == null) {
|
if (options.proxy == null) {
|
||||||
// No proxy needed
|
// No proxy needed
|
||||||
|
|
@ -1584,7 +1588,7 @@ function createAuthenticodeHandler(path) {
|
||||||
|
|
||||||
// Set up the request
|
// Set up the request
|
||||||
var responseAccumulator = '';
|
var responseAccumulator = '';
|
||||||
var req = require('http').request(options, function (res) {
|
var req = http.request(options, function (res) {
|
||||||
res.setEncoding('utf8');
|
res.setEncoding('utf8');
|
||||||
res.on('data', function (chunk) { responseAccumulator += chunk; });
|
res.on('data', function (chunk) { responseAccumulator += chunk; });
|
||||||
res.on('end', function () { func(null, responseAccumulator); });
|
res.on('end', function () { func(null, responseAccumulator); });
|
||||||
|
|
@ -1605,12 +1609,12 @@ function createAuthenticodeHandler(path) {
|
||||||
proxyOptions.protocol = proxyUrl.protocol;
|
proxyOptions.protocol = proxyUrl.protocol;
|
||||||
proxyOptions.hostname = proxyUrl.hostname;
|
proxyOptions.hostname = proxyUrl.hostname;
|
||||||
proxyOptions.path = options.hostname + ':' + options.port;
|
proxyOptions.path = options.hostname + ':' + options.port;
|
||||||
proxyOptions.port = ((proxyUrl.port == '') ? 80 : parseInt(proxyUrl.port));
|
proxyOptions.port = ((proxyUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(proxyUrl.port));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the proxy request
|
// Set up the proxy request
|
||||||
var responseAccumulator = '';
|
var responseAccumulator = '';
|
||||||
var req = require('http').request(proxyOptions);
|
var req = http.request(proxyOptions);
|
||||||
req.on('error', function (err) { func('' + err); });
|
req.on('error', function (err) { func('' + err); });
|
||||||
req.on('connect', function (res, socket, head) {
|
req.on('connect', function (res, socket, head) {
|
||||||
// Make a request over the HTTP tunnel
|
// Make a request over the HTTP tunnel
|
||||||
|
|
|
||||||
|
|
@ -1049,6 +1049,7 @@ module.exports.CertificateOperations = function (parent) {
|
||||||
config.domains[i].certs = r.dns[i];
|
config.domains[i].certs = r.dns[i];
|
||||||
} else {
|
} else {
|
||||||
console.log("WARNING: File \"webserver-" + i + "-cert-public.crt\" missing, domain \"" + i + "\" will not work correctly.");
|
console.log("WARNING: File \"webserver-" + i + "-cert-public.crt\" missing, domain \"" + i + "\" will not work correctly.");
|
||||||
|
rcountmax++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If the web certificate already exist, load it. Load both certificate and private key
|
// If the web certificate already exist, load it. Load both certificate and private key
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,12 @@ module.exports.objKeysToLower = function (obj, exceptions, parent) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Escape and unescape field names so there are no invalid characters for MongoDB
|
// Escape and unescape field names so there are no invalid characters for MongoDB/NeDB ("$", ",", ".", see https://github.com/seald/nedb/tree/master?tab=readme-ov-file#inserting-documents)
|
||||||
module.exports.escapeFieldName = function (name) { if ((name.indexOf('%') == -1) && (name.indexOf('.') == -1) && (name.indexOf('$') == -1)) return name; return name.split('%').join('%25').split('.').join('%2E').split('$').join('%24'); };
|
module.exports.escapeFieldName = function (name) { if ((name.indexOf(',') == -1) && (name.indexOf('%') == -1) && (name.indexOf('.') == -1) && (name.indexOf('$') == -1)) return name; return name.split('%').join('%25').split('.').join('%2E').split('$').join('%24').split(',').join('%2C'); };
|
||||||
module.exports.unEscapeFieldName = function (name) { if (name.indexOf('%') == -1) return name; return name.split('%2E').join('.').split('%24').join('$').split('%25').join('%'); };
|
module.exports.unEscapeFieldName = function (name) { if (name.indexOf('%') == -1) return name; return name.split('%2C').join(',').split('%2E').join('.').split('%24').join('$').split('%25').join('%'); };
|
||||||
|
|
||||||
// Escape all links, SSH and RDP usernames
|
// Escape all links, SSH and RDP usernames
|
||||||
// This is required for databases like NeDB that don't accept "." as part of a field name.
|
// This is required for databases like NeDB that don't accept "." or "," as part of a field name.
|
||||||
module.exports.escapeLinksFieldNameEx = function (docx) { if ((docx.links == null) && (docx.ssh == null) && (docx.rdp == null)) { return docx; } return module.exports.escapeLinksFieldName(docx); };
|
module.exports.escapeLinksFieldNameEx = function (docx) { if ((docx.links == null) && (docx.ssh == null) && (docx.rdp == null)) { return docx; } return module.exports.escapeLinksFieldName(docx); };
|
||||||
module.exports.escapeLinksFieldName = function (docx) {
|
module.exports.escapeLinksFieldName = function (docx) {
|
||||||
var doc = Object.assign({}, docx);
|
var doc = Object.assign({}, docx);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
"@yetzt/nedb": "1.8.0",
|
"@seald-io/nedb": "4.0.4",
|
||||||
"archiver": "7.0.0",
|
"archiver": "7.0.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"cbor": "5.2.0",
|
"cbor": "5.2.0",
|
||||||
"compression": "1.7.4",
|
"compression": "1.7.5",
|
||||||
"cookie-session": "2.0.0",
|
"cookie-session": "2.1.0",
|
||||||
"express": "4.19.2",
|
"express": "4.21.2",
|
||||||
"express-handlebars": "5.3.5",
|
"express-handlebars": "7.1.3",
|
||||||
"express-ws": "5.0.2",
|
"express-ws": "5.0.2",
|
||||||
"ipcheck": "0.1.0",
|
"ipcheck": "0.1.0",
|
||||||
"minimist": "1.2.8",
|
"minimist": "1.2.8",
|
||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"ua-parser-js": "1.0.37",
|
"ua-parser-js": "1.0.39",
|
||||||
"ws": "8.17.1",
|
"ws": "8.18.0",
|
||||||
"yauzl": "2.10.0"
|
"yauzl": "2.10.0"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
|
||||||
|
|
||||||
RUN mkdir -p /opt/meshcentral/meshcentral
|
RUN mkdir -p /opt/meshcentral/meshcentral
|
||||||
COPY ./ /opt/meshcentral/meshcentral/
|
COPY ./ /opt/meshcentral/meshcentral/
|
||||||
|
|
@ -18,7 +18,7 @@ RUN if ! [ -z "$DISABLE_TRANSLATE" ] && [ "$DISABLE_TRANSLATE" != "yes" ] && [ "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# install translate/minify modules if need too
|
# install translate/minify modules if need too
|
||||||
RUN if [ -z "$DISABLE_MINIFY" ] || [ -z "$DISABLE_TRANSLATE" ]; then cd meshcentral && npm install html-minifier jsdom minify-js; fi
|
RUN if [ -z "$DISABLE_MINIFY" ] || [ -z "$DISABLE_TRANSLATE" ]; then cd meshcentral && npm install html-minifier@4.0.0 jsdom@22.1.0 esprima@4.0.1; fi
|
||||||
|
|
||||||
# first extractall if need too
|
# first extractall if need too
|
||||||
RUN if [ -z "$DISABLE_MINIFY" ] || [ -z "$DISABLE_TRANSLATE" ]; then cd meshcentral/translate && node translate.js extractall; fi
|
RUN if [ -z "$DISABLE_MINIFY" ] || [ -z "$DISABLE_TRANSLATE" ]; then cd meshcentral/translate && node translate.js extractall; fi
|
||||||
|
|
@ -34,7 +34,7 @@ RUN rm -rf /opt/meshcentral/meshcentral/docker
|
||||||
RUN rm -rf /opt/meshcentral/meshcentral/node_modules
|
RUN rm -rf /opt/meshcentral/meshcentral/node_modules
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM alpine:3.19
|
FROM --platform=$TARGETPLATFORM alpine:3.21
|
||||||
|
|
||||||
#Add non-root user, add installation directories and assign proper permissions
|
#Add non-root user, add installation directories and assign proper permissions
|
||||||
RUN mkdir -p /opt/meshcentral/meshcentral
|
RUN mkdir -p /opt/meshcentral/meshcentral
|
||||||
|
|
@ -62,8 +62,8 @@ ENV MONGO_URL=""
|
||||||
ENV HOSTNAME="localhost"
|
ENV HOSTNAME="localhost"
|
||||||
ENV ALLOW_NEW_ACCOUNTS="true"
|
ENV ALLOW_NEW_ACCOUNTS="true"
|
||||||
ENV ALLOWPLUGINS="false"
|
ENV ALLOWPLUGINS="false"
|
||||||
ENV LOCALSESSIONRECORDING="false"
|
ENV LOCALSESSIONRECORDING="true"
|
||||||
ENV MINIFY="true"
|
ENV MINIFY="false"
|
||||||
ENV WEBRTC="false"
|
ENV WEBRTC="false"
|
||||||
ENV IFRAME="false"
|
ENV IFRAME="false"
|
||||||
ENV SESSION_KEY=""
|
ENV SESSION_KEY=""
|
||||||
|
|
@ -83,12 +83,12 @@ COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
|
||||||
COPY ./docker/startup.sh ./startup.sh
|
COPY ./docker/startup.sh ./startup.sh
|
||||||
COPY ./docker/config.json.template /opt/meshcentral/config.json.template
|
COPY ./docker/config.json.template /opt/meshcentral/config.json.template
|
||||||
|
|
||||||
# install dependencies from package.json and nedb
|
# install dependencies from package.json
|
||||||
RUN cd meshcentral && npm install && npm install nedb
|
RUN cd meshcentral && npm install
|
||||||
|
|
||||||
# NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN meshcentral.js mainStart()
|
# NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN meshcentral.js mainStart()
|
||||||
RUN if ! [ -z "$INCLUDE_MONGODBTOOLS" ]; then cd meshcentral && npm install mongodb@4.13.0 saslprep@1.0.3; fi
|
RUN if ! [ -z "$INCLUDE_MONGODBTOOLS" ]; then cd meshcentral && npm install mongodb@4.13.0 saslprep@1.0.3; fi
|
||||||
RUN if ! [ -z "$PREINSTALL_LIBS" ] && [ "$PREINSTALL_LIBS" == "true" ]; then cd meshcentral && npm install ssh2@1.15.0 semver@7.5.4 nodemailer@6.9.8 image-size@1.0.2 wildleek@2.0.0 otplib@10.2.3 yubikeyotp@0.2.0; fi
|
RUN if ! [ -z "$PREINSTALL_LIBS" ] && [ "$PREINSTALL_LIBS" == "true" ]; then cd meshcentral && npm install ssh2@1.16.0 semver@7.5.4 nodemailer@6.9.15 image-size@1.1.1 wildleek@2.0.0 otplib@10.2.3 yubikeyotp@0.2.0; fi
|
||||||
|
|
||||||
EXPOSE 80 443 4433
|
EXPOSE 80 443 4433
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@
|
||||||
"": {
|
"": {
|
||||||
"_title": "MyServer",
|
"_title": "MyServer",
|
||||||
"_title2": "Servername",
|
"_title2": "Servername",
|
||||||
"minify": true,
|
"minify": false,
|
||||||
"NewAccounts": true,
|
"NewAccounts": true,
|
||||||
"localSessionRecording": false,
|
"localSessionRecording": true,
|
||||||
"_userNameIsEmail": true,
|
"_userNameIsEmail": true,
|
||||||
"_certUrl": "my.reverse.proxy"
|
"_certUrl": "my.reverse.proxy"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ else
|
||||||
sed -i "s/\"NewAccounts\": true/\"NewAccounts\": $ALLOW_NEW_ACCOUNTS/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"NewAccounts\": true/\"NewAccounts\": $ALLOW_NEW_ACCOUNTS/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
sed -i "s/\"enabled\": false/\"enabled\": $ALLOWPLUGINS/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"enabled\": false/\"enabled\": $ALLOWPLUGINS/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
sed -i "s/\"localSessionRecording\": false/\"localSessionRecording\": $LOCALSESSIONRECORDING/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"localSessionRecording\": false/\"localSessionRecording\": $LOCALSESSIONRECORDING/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
sed -i "s/\"minify\": true/\"minify\": $MINIFY/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"minify\": false/\"minify\": $MINIFY/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
sed -i "s/\"WebRTC\": false/\"WebRTC\": $WEBRTC/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"WebRTC\": false/\"WebRTC\": $WEBRTC/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
sed -i "s/\"AllowFraming\": false/\"AllowFraming\": $IFRAME/" meshcentral-data/"${CONFIG_FILE}"
|
sed -i "s/\"AllowFraming\": false/\"AllowFraming\": $IFRAME/" meshcentral-data/"${CONFIG_FILE}"
|
||||||
if [ -z "$SESSION_KEY" ]; then
|
if [ -z "$SESSION_KEY" ]; then
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ For more information, [visit MeshCentral.com](https://www.meshcentral.com/).
|
||||||
|
|
||||||
[Reddit](https://www.reddit.com/r/MeshCentral/)
|
[Reddit](https://www.reddit.com/r/MeshCentral/)
|
||||||
|
|
||||||
[Twitter](https://twitter.com/MeshCentral)
|
[BlueSky](https://bsky.app/profile/meshcentral.bsky.social)
|
||||||
|
|
||||||
[BlogSpot](https://meshcentral2.blogspot.com/)
|
[BlogSpot](https://meshcentral2.blogspot.com/)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@ chmod 755 mc-azure-ubuntu1804.sh
|
||||||
|
|
||||||
In this situation, port 3389 will be used to receive Intel AMT CIRA connections instead of port 4433. After these scripts are run, try accessing the server using a browser. MeshCentral will take a minute or two to create certificates after that, the server will be up. The first account to be created will be the site administrator – so don’t delay and create an account right away. Once running, move on to the MeshCentral’s user’s guide to configure your new server.
|
In this situation, port 3389 will be used to receive Intel AMT CIRA connections instead of port 4433. After these scripts are run, try accessing the server using a browser. MeshCentral will take a minute or two to create certificates after that, the server will be up. The first account to be created will be the site administrator – so don’t delay and create an account right away. Once running, move on to the MeshCentral’s user’s guide to configure your new server.
|
||||||
|
|
||||||
|
### Elestio
|
||||||
|
|
||||||
|
You can deploy MeshCentral on Elestio using one-click deployment. Elestio handles version updates, maintenance, securtiy, backups, etc. Additionally, Elestio supports MeshCentral by providing revenue share so go ahead and click below to deploy and start using.
|
||||||
|
|
||||||
|
[](https://elest.io/open-source/meshcentral)
|
||||||
|
|
||||||
## Server Security - Adding Crowdsec
|
## Server Security - Adding Crowdsec
|
||||||
|
|
||||||
MeshCentral has built-in support for a CrowdSec bouncer. This allows MeshCentral to get threat signals from the community and block or CAPTCHA requests coming from known bad IP addresses.
|
MeshCentral has built-in support for a CrowdSec bouncer. This allows MeshCentral to get threat signals from the community and block or CAPTCHA requests coming from known bad IP addresses.
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,55 @@ If you're getting a `port 4433 is not available` error, this is because another
|
||||||
|
|
||||||
In general the problem is that you are running two MeshCentral instances at the same time. Probably one as a background Windows Service and one in the command line. Which ever instance can grab port 4433 will have a running MPS and CIRA should work, but the second instance will not have port 4433 and CIRA will not work.
|
In general the problem is that you are running two MeshCentral instances at the same time. Probably one as a background Windows Service and one in the command line. Which ever instance can grab port 4433 will have a running MPS and CIRA should work, but the second instance will not have port 4433 and CIRA will not work.
|
||||||
|
|
||||||
|
### Running Meshcentral server in debug mode
|
||||||
|
|
||||||
|
Debug more will cause MeshCentral to output a lot of debug messages to the console. To display all debug messages, run MeshCentral like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node node_modules/meshcentral --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
A more practical way to run the debug command it to specify what messages you want printed out using a comma seperated list, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node node_modules/meshcentral --debug web,amt,mps
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the list of all debug options:
|
||||||
|
|
||||||
|
```
|
||||||
|
cookie - Cookie encoder
|
||||||
|
dispatch - Message Dispatcher
|
||||||
|
main - Main Server Messages
|
||||||
|
peer - MeshCentral Server Peering
|
||||||
|
agent - MeshAgent traffic
|
||||||
|
agentupdate - MeshAgent update
|
||||||
|
cert - Server Certificate
|
||||||
|
db - Server Database
|
||||||
|
email - Email/SMS/Push Traffic
|
||||||
|
web - Web Server
|
||||||
|
webrequest - Web Server Requests
|
||||||
|
relay - Web Socket Relay
|
||||||
|
httpheaders - Web Server HTTP Headers
|
||||||
|
authlog - User Authentication Log
|
||||||
|
amt - Intel AMT
|
||||||
|
webrelay - Connection Relay
|
||||||
|
mps - CIRA Server
|
||||||
|
mpscmd - CIRA Server Commands
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also specify the `debug` option in the config.json file in the `settings` section. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
"settings": {
|
||||||
|
"debug": "web,amt,mps"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Enabling trace in your browser Dev Tools
|
### Enabling trace in your browser Dev Tools
|
||||||
|
|
||||||
`Trace=1` as a parameter in chrome dev tools for debugging
|
You can enable browser console tracing by adding `trace=1` as a parameter to the URL of the MeshCentral main web page. For example `https://myserver.com/?trace=1`. Once present, open the browser's console window to see all web client tracing messages.
|
||||||
|
|
||||||
|
|
||||||
To log all database queries, change log_statement in /etc/postgresql/13/main/postgresql.conf
|
To log all database queries, change log_statement in /etc/postgresql/13/main/postgresql.conf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Meshcentral2 Guide
|
# MeshCentral Guide
|
||||||
|
|
||||||
[MeshCentral2 Guide](https://meshcentral.com/docs/MeshCentral2UserGuide.pdf)
|
[MeshCentral Guide](https://meshcentral.com/docs/MeshCentral2UserGuide.pdf)
|
||||||
|
|
||||||
MeshCmd Guide [as .pdf](https://meshcentral.com/docs/MeshCmdUserGuide.pdf) [as .odt](https://github.com/Ylianst/MeshCentral/blob/master/docs/MeshCentral User's Guide v0.2.9.odt?raw=true)
|
MeshCmd Guide [as .pdf](https://meshcentral.com/docs/MeshCmdUserGuide.pdf) [as .odt](https://github.com/Ylianst/MeshCentral/blob/master/docs/MeshCentral User's Guide v0.2.9.odt?raw=true)
|
||||||
|
|
||||||
|
|
@ -1278,6 +1278,8 @@ And taking authentication to the next step is removing the login page entirely.
|
||||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/-WKY8Wy0Huk" frameborder="0" allowfullscreen></iframe>
|
<iframe width="320" height="180" src="https://www.youtube.com/embed/-WKY8Wy0Huk" frameborder="0" allowfullscreen></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
You can also setup [Duo 2FA](https://github.com/Ylianst/MeshCentral/blob/master/docs/docs/meshcentral/security.md#duo-2fa-setup) which is a commertial offering.
|
||||||
|
|
||||||
## Server Backup & Restore
|
## Server Backup & Restore
|
||||||
|
|
||||||
It’s very important that the server be backed up regularly and that a backup be kept offsite. Luckily, performing a full backup of the MeshCentral server is generally easy to do. For all installations make sure to back up the following two folders and all sub-folders.
|
It’s very important that the server be backed up regularly and that a backup be kept offsite. Luckily, performing a full backup of the MeshCentral server is generally easy to do. For all installations make sure to back up the following two folders and all sub-folders.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,23 @@ OpenID Connect allows clients of all types, including Web-based, mobile, and Jav
|
||||||
|
|
||||||
That description was straight from [OpenID Connect Documentation](https://openid.net/connect/), but basically, OAuth2 is the foundation upon which OpenID Connect was built, allowing for wide ranging compatability and interconnection. OpenID Connect appends the secure user *authentication* OAuth2 is known for, with user *authorization* by allowing the request of additional *scopes* that provide additional *claims* or access to API's in an easily expandable way.
|
That description was straight from [OpenID Connect Documentation](https://openid.net/connect/), but basically, OAuth2 is the foundation upon which OpenID Connect was built, allowing for wide ranging compatability and interconnection. OpenID Connect appends the secure user *authentication* OAuth2 is known for, with user *authorization* by allowing the request of additional *scopes* that provide additional *claims* or access to API's in an easily expandable way.
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
|
||||||
|
#### Own IDP, CA and Docker
|
||||||
|
|
||||||
|
If you operate your own identity provider, your own certification authority and MeshCentral via Docker, it is necessary to provide the complete certificate chain, otherwise NodeJS (in particular the openid-client module) will refuse the connection to the IDP server.
|
||||||
|
|
||||||
|
The following errors can be found in the log file:
|
||||||
|
> OIDC: Discovery failed.
|
||||||
|
|
||||||
|
> UNABLE_TO_GET_ISSUER_CERT_LOCALLY
|
||||||
|
|
||||||
|
To solve this problem, the certificate chain in PEM format must be placed in the data directory and the following entry must be added to the docker-compose.yml file in the “environment” section:
|
||||||
|
```
|
||||||
|
environment:
|
||||||
|
- NODE_EXTRA_CA_CERTS=/opt/meshcentral/meshcentral-data/chain.pem
|
||||||
|
```
|
||||||
|
|
||||||
## Basic Config
|
## Basic Config
|
||||||
|
|
||||||
### *Introduction*
|
### *Introduction*
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,10 @@ Use of the optional file `plugin_name.js` in the optional folder `modules_meshco
|
||||||
Much of MeshCentral revolves around returning objects for your structures, and plugins are no different. Within your plugin you can traverse all the way up to the web server and MeshCentral Server classes to access all the functionality those layers provide. This is done by passing the current object to newly created objects, and assigning that reference to a `parent` variable within that object.
|
Much of MeshCentral revolves around returning objects for your structures, and plugins are no different. Within your plugin you can traverse all the way up to the web server and MeshCentral Server classes to access all the functionality those layers provide. This is done by passing the current object to newly created objects, and assigning that reference to a `parent` variable within that object.
|
||||||
|
|
||||||
|
|
||||||
|
## Ping-Pong
|
||||||
|
|
||||||
|
If you build a plugin which makes use of `meshrelay.ashx`, keep in mind to either handle ping-pong messages (`serverPing`, `serverPong`) on the control channel or to request MeshCentral to not send such messages through sending the `noping=1` parameter in the connection URL. For a deeper sight search for "PING/PONG" in `meshrelay.js`.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
Versioning your plugin correctly and consistently is essential to ensure users of your plugin are prompted to upgrade when it is available. Semantic versioning is recommended.
|
Versioning your plugin correctly and consistently is essential to ensure users of your plugin are prompted to upgrade when it is available. Semantic versioning is recommended.
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,29 @@ Adjust these items in your `config.json`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Duo 2FA setup
|
||||||
|
|
||||||
|
MeshCentral supports Duo as a way for users to add two-factor authentication and Duo offers free accounts for user 10 users. To get started, go to [Duo.com](https://duo.com/) and create a free account. Once logged into Duo, select "Applications" and "Protect an Application" on the left side. Search for "Web SDK" and hit the "Protect" button. You will see a screen with the following information:
|
||||||
|
|
||||||
|
- Client ID
|
||||||
|
- Client secret
|
||||||
|
- API hostname
|
||||||
|
|
||||||
|
Copy these three values in a safe place and do not share these values with anyone. Then, in your MeshCentral config.json file, add the following in the domains section:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"domains": {
|
||||||
|
"": {
|
||||||
|
"duo2factor": {
|
||||||
|
"integrationkey": "ClientId",
|
||||||
|
"secretkey": "ClientSecret",
|
||||||
|
"apihostname": "api-xxxxxxxxxxx.duosecurity.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart MeshCentral and your server should now be Duo capable. Users will see an option to enable it in the "My Account" tab. When enabling it, users will be walked thru the process of downloading the mobile application and going thru a trial run on 2FA. Users that get setup will be added to your Duo account under the "Users" / "Users" screen in Duo. Note that the "admin" user is not valid in Duo, so, if you have a user with the name "Admin" in MeshCentral, they will get an error trying to setup Duo.
|
||||||
|
|
|
||||||
188
firebase.js
188
firebase.js
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @description MeshCentral Firebase communication module
|
* @description MeshCentral Firebase communication module
|
||||||
* @author Ylian Saint-Hilaire
|
* @author Ylian Saint-Hilaire
|
||||||
* @copyright Intel Corporation 2018-2022
|
|
||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
* @version v0.0.1
|
* @version v0.0.1
|
||||||
*/
|
*/
|
||||||
|
|
@ -14,27 +13,31 @@
|
||||||
/*jshint esversion: 6 */
|
/*jshint esversion: 6 */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Construct the Firebase object
|
// Initialize the Firebase Admin SDK
|
||||||
module.exports.CreateFirebase = function (parent, senderid, serverkey) {
|
module.exports.CreateFirebase = function (parent, serviceAccount) {
|
||||||
var obj = {};
|
|
||||||
|
// Import the Firebase Admin SDK
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
|
||||||
|
const obj = {};
|
||||||
obj.messageId = 0;
|
obj.messageId = 0;
|
||||||
obj.relays = {};
|
obj.relays = {};
|
||||||
obj.stats = {
|
obj.stats = {
|
||||||
mode: "Real",
|
mode: 'Real',
|
||||||
sent: 0,
|
sent: 0,
|
||||||
sendError: 0,
|
sendError: 0,
|
||||||
received: 0,
|
received: 0,
|
||||||
receivedNoRoute: 0,
|
receivedNoRoute: 0,
|
||||||
receivedBadArgs: 0
|
receivedBadArgs: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenToNodeMap = {}; // Token --> { nid: nodeid, mid: meshid }
|
||||||
|
|
||||||
|
// Initialize Firebase Admin with server key and project ID
|
||||||
|
if (!admin.apps.length) {
|
||||||
|
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sender = require('node-xcs').Sender;
|
|
||||||
const Message = require('node-xcs').Message;
|
|
||||||
const Notification = require('node-xcs').Notification;
|
|
||||||
const xcs = new Sender(senderid, serverkey);
|
|
||||||
|
|
||||||
var tokenToNodeMap = {} // Token --> { nid: nodeid, mid: meshid }
|
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
if (parent.config.firebase && (parent.config.firebase.log === true)) {
|
if (parent.config.firebase && (parent.config.firebase.log === true)) {
|
||||||
obj.logpath = parent.path.join(parent.datapath, 'firebase.txt');
|
obj.logpath = parent.path.join(parent.datapath, 'firebase.txt');
|
||||||
|
|
@ -43,133 +46,85 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
|
||||||
obj.log = function () { }
|
obj.log = function () { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages received from client (excluding receipts)
|
// Function to send notifications
|
||||||
xcs.on('message', function (messageId, from, data, category) {
|
|
||||||
const jsonData = JSON.stringify(data);
|
|
||||||
obj.log('Firebase-Message: ' + jsonData);
|
|
||||||
parent.debug('email', 'Firebase-Message: ' + jsonData);
|
|
||||||
|
|
||||||
if (typeof data.r == 'string') {
|
|
||||||
// Lookup push relay server
|
|
||||||
parent.debug('email', 'Firebase-RelayRoute: ' + data.r);
|
|
||||||
const wsrelay = obj.relays[data.r];
|
|
||||||
if (wsrelay != null) {
|
|
||||||
delete data.r;
|
|
||||||
try { wsrelay.send(JSON.stringify({ from: from, data: data, category: category })); } catch (ex) { }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Lookup node information from the cache
|
|
||||||
var ninfo = tokenToNodeMap[from];
|
|
||||||
if (ninfo == null) { obj.stats.receivedNoRoute++; return; }
|
|
||||||
|
|
||||||
if ((data != null) && (data.con != null) && (data.s != null)) { // Console command
|
|
||||||
obj.stats.received++;
|
|
||||||
parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid);
|
|
||||||
} else {
|
|
||||||
obj.stats.receivedBadArgs++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only fired for messages where options.delivery_receipt_requested = true
|
|
||||||
/*
|
|
||||||
xcs.on('receipt', function (messageId, from, data, category) { console.log('Firebase-Receipt', messageId, from, data, category); });
|
|
||||||
xcs.on('connected', function () { console.log('Connected'); });
|
|
||||||
xcs.on('disconnected', function () { console.log('disconnected'); });
|
|
||||||
xcs.on('online', function () { console.log('online'); });
|
|
||||||
xcs.on('error', function (e) { console.log('error', e); });
|
|
||||||
xcs.on('message-error', function (e) { console.log('message-error', e); });
|
|
||||||
*/
|
|
||||||
|
|
||||||
xcs.start();
|
|
||||||
|
|
||||||
obj.log('CreateFirebase-Setup');
|
|
||||||
parent.debug('email', 'CreateFirebase-Setup');
|
|
||||||
|
|
||||||
// EXAMPLE
|
|
||||||
//var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } };
|
|
||||||
//var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High'
|
|
||||||
|
|
||||||
obj.sendToDevice = function (node, payload, options, func) {
|
obj.sendToDevice = function (node, payload, options, func) {
|
||||||
if (typeof node == 'string') {
|
if (typeof node === 'string') {
|
||||||
parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } })
|
parent.db.Get(node, function (err, docs) {
|
||||||
|
if (!err && docs && docs.length === 1) {
|
||||||
|
obj.sendToDeviceEx(docs[0], payload, options, func);
|
||||||
|
} else {
|
||||||
|
func(0, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
obj.sendToDeviceEx(node, payload, options, func);
|
obj.sendToDeviceEx(node, payload, options, func);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Send an outbound push notification
|
// Send an outbound push notification
|
||||||
obj.sendToDeviceEx = function (node, payload, options, func) {
|
obj.sendToDeviceEx = function (node, payload, options, func) {
|
||||||
parent.debug('email', 'Firebase-sendToDevice');
|
if (!node || typeof node.pmt !== 'string') {
|
||||||
if ((node == null) || (typeof node.pmt != 'string')) return;
|
func(0, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
|
obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
|
||||||
|
|
||||||
// Fill in our lookup table
|
// Fill in our lookup table
|
||||||
if (node._id != null) { tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } }
|
if (node._id) {
|
||||||
|
tokenToNodeMap[node.pmt] = {
|
||||||
// Built the on-screen notification
|
nid: node._id,
|
||||||
var notification = null;
|
mid: node.meshid,
|
||||||
if (payload.notification) {
|
did: node.domain
|
||||||
var notification = new Notification('ic_message')
|
};
|
||||||
.title(payload.notification.title)
|
|
||||||
.body(payload.notification.body)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the message
|
const message = {
|
||||||
var message = new Message('msg_' + (++obj.messageId));
|
token: node.pmt,
|
||||||
if (options.priority) { message.priority(options.priority); }
|
notification: payload.notification,
|
||||||
if (payload.data) { for (var i in payload.data) { message.addData(i, payload.data[i]); } }
|
data: payload.data,
|
||||||
if ((payload.data == null) || (payload.data.shash == null)) { message.addData('shash', parent.webserver.agentCertificateHashBase64); } // Add the server agent hash, new Android agents will reject notifications that don't have this.
|
android: {
|
||||||
if (notification) { message.notification(notification) }
|
priority: options.priority || 'high',
|
||||||
message.build();
|
ttl: options.timeToLive ? options.timeToLive * 1000 : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Send the message
|
admin.messaging().send(message).then(function (response) {
|
||||||
function callback(result) {
|
obj.stats.sent++;
|
||||||
if (result.getError() == null) { obj.stats.sent++; obj.log('Success'); } else { obj.stats.sendError++; obj.log('Fail'); }
|
obj.log('Success');
|
||||||
callback.func(result.getMessageId(), result.getError(), result.getErrorDescription())
|
func(response);
|
||||||
}
|
}).catch(function (error) {
|
||||||
callback.func = func;
|
obj.stats.sendError++;
|
||||||
parent.debug('email', 'Firebase-sending');
|
obj.log('Fail: ' + error);
|
||||||
xcs.sendNoRetry(message, node.pmt, callback);
|
func(0, error);
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Setup a two way relay
|
// Setup a two way relay
|
||||||
obj.setupRelay = function (ws) {
|
obj.setupRelay = function (ws) {
|
||||||
// Select and set a relay identifier
|
|
||||||
ws.relayId = getRandomPassword();
|
ws.relayId = getRandomPassword();
|
||||||
while (obj.relays[ws.relayId] != null) { ws.relayId = getRandomPassword(); }
|
while (obj.relays[ws.relayId]) { ws.relayId = getRandomPassword(); }
|
||||||
obj.relays[ws.relayId] = ws;
|
obj.relays[ws.relayId] = ws;
|
||||||
|
|
||||||
// On message, parse it
|
|
||||||
ws.on('message', function (msg) {
|
ws.on('message', function (msg) {
|
||||||
parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg);
|
parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg);
|
||||||
if (typeof msg == 'string') {
|
if (typeof msg === 'string') {
|
||||||
obj.log('Relay: ' + msg);
|
obj.log('Relay: ' + msg);
|
||||||
|
|
||||||
// Parse the incoming push request
|
let data;
|
||||||
var data = null;
|
try { data = JSON.parse(msg); } catch (ex) { return; }
|
||||||
try { data = JSON.parse(msg) } catch (ex) { return; }
|
if (typeof data !== 'object') return;
|
||||||
if (typeof data != 'object') return;
|
if (!parent.common.validateObjectForMongo(data, 4096)) return;
|
||||||
if (parent.common.validateObjectForMongo(data, 4096) == false) return; // Perform sanity checking on this object.
|
if (typeof data.pmt !== 'string' || typeof data.payload !== 'object') return;
|
||||||
if (typeof data.pmt != 'string') return;
|
|
||||||
if (typeof data.payload != 'object') return;
|
|
||||||
if (typeof data.payload.notification == 'object') {
|
|
||||||
if (typeof data.payload.notification.title != 'string') return;
|
|
||||||
if (typeof data.payload.notification.body != 'string') return;
|
|
||||||
}
|
|
||||||
if (typeof data.options != 'object') return;
|
|
||||||
if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) return;
|
|
||||||
if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) return;
|
|
||||||
if (typeof data.payload.data != 'object') { data.payload.data = {}; }
|
|
||||||
data.payload.data.r = ws.relayId; // Set the relay id.
|
|
||||||
|
|
||||||
// Send the push notification
|
data.payload.data = data.payload.data || {};
|
||||||
obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
|
data.payload.data.r = ws.relayId;
|
||||||
if (err == null) {
|
|
||||||
try { wsrelay.send(JSON.stringify({ sent: true })); } catch (ex) { }
|
obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err) {
|
||||||
|
if (!err) {
|
||||||
|
try { ws.send(JSON.stringify({ sent: true })); } catch (ex) { }
|
||||||
} else {
|
} else {
|
||||||
try { wsrelay.send(JSON.stringify({ sent: false })); } catch (ex) { }
|
try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -186,11 +141,12 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
|
||||||
parent.debug('email', 'FBWS-Close(' + this.relayId + ')');
|
parent.debug('email', 'FBWS-Close(' + this.relayId + ')');
|
||||||
delete obj.relays[this.relayId];
|
delete obj.relays[this.relayId];
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRandomPassword() {
|
||||||
|
return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').replace(/\//g, '@');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
|
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
2
mcrec.js
2
mcrec.js
|
|
@ -321,7 +321,7 @@ function setup() { InstallModules(['image-size'], start); }
|
||||||
function start() { startEx(process.argv); }
|
function start() { startEx(process.argv); }
|
||||||
function startEx(argv) {
|
function startEx(argv) {
|
||||||
if (argv.length > 2) { indexFile(argv[2]); } else {
|
if (argv.length > 2) { indexFile(argv[2]); } else {
|
||||||
log("MeshCentral Session Recodings Processor");
|
log("MeshCentral Session Recordings Processor");
|
||||||
log("This tool will index a .mcrec file so that the player can seek thru the file.");
|
log("This tool will index a .mcrec file so that the player can seek thru the file.");
|
||||||
log("");
|
log("");
|
||||||
log(" Usage: node mcrec [file]");
|
log(" Usage: node mcrec [file]");
|
||||||
|
|
|
||||||
|
|
@ -616,7 +616,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
|
if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
|
||||||
const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser.toLowerCase()];
|
const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser];
|
||||||
if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
|
if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
|
||||||
// Mesh name is hex instead of base64
|
// Mesh name is hex instead of base64
|
||||||
const meshname = obj.meshid.substring(0, 18);
|
const meshname = obj.meshid.substring(0, 18);
|
||||||
|
|
@ -1938,6 +1938,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
|
if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
|
||||||
|
if ((command.lusers != null) && (Array.isArray(command.lusers)) && (device.lusers != command.lusers)) { device.lusers = command.lusers; change = 1; } // Don't save this to the db.
|
||||||
if ((mesh.mtype == 2) && (!args.wanonly)) {
|
if ((mesh.mtype == 2) && (!args.wanonly)) {
|
||||||
// In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
|
// In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
|
||||||
if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
|
if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,41 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sqlite3": {
|
"sqlite3": {
|
||||||
"type": "boolean",
|
"type": [ "boolean", "string", "object" ],
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "Set true to use SQLite3 as a local MeshCentral database."
|
"description": "Set boolean true to use SQLite3 as a local MeshCentral database with default db filename 'meshcentral' or enter a string for a different db filename. Extension .sqlite is appended",
|
||||||
|
"properties":{
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "meshcentral",
|
||||||
|
"description": "Database filename. '.sqlite' is appended"
|
||||||
|
},
|
||||||
|
"journalMode": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "delete",
|
||||||
|
"description": "DELETE, TRUNCATE, PERSIST, MEMORY, WAL. NONE not allowed. See: https://www.sqlite.org/pragma.html#pragma_journal_mode"
|
||||||
|
},
|
||||||
|
"journalSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 4096000,
|
||||||
|
"description": "Maximum size of the journal file in bytes. Can grow larger if needed, but will shrink to this size. -1 is unlimited growth. See: https://www.sqlite.org/pragma.html#pragma_journal_size_limit"
|
||||||
|
},
|
||||||
|
"autoVacuum": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "incremental",
|
||||||
|
"description": "none, full, incremental. Removes unused pages and shrinks databasefile during maintenance. See: https://www.sqlite.org/pragma.html#pragma_auto_vacuum"
|
||||||
|
},
|
||||||
|
"incrementalVacuum": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Maximum amount of pages to free during maintenance. Default page size is 4k, so default frees up to 400k from the databasefile. See: https://www.sqlite.org/pragma.html#pragma_incremental_vacuum"
|
||||||
|
},
|
||||||
|
"startupVacuum": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Do a full VACUUM at startup. Shrinks the db file and optimizes it. This can take some time with a large database and can temporarily take up to double the database size on disk. See: https://www.sqlite.org/lang_vacuum.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"mySQL": {
|
"mySQL": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -488,11 +520,6 @@
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "When enabled, MeshCentral will automatically monitor and manage Intel AMT devices."
|
"description": "When enabled, MeshCentral will automatically monitor and manage Intel AMT devices."
|
||||||
},
|
},
|
||||||
"orphanAgentUser": {
|
|
||||||
"type": "string",
|
|
||||||
"default": null,
|
|
||||||
"description": "If an agent attempts to connect to a unknown device group, automatically create a new device group and grant access to the specified user. Example: admin"
|
|
||||||
},
|
|
||||||
"agentIdleTimeout": {
|
"agentIdleTimeout": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
|
|
@ -740,13 +767,27 @@
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "When set to true, the MPS server will only accept TLS 1.2 and 1.3 connections. Older Intel AMT devices will not be able to connect."
|
"description": "When set to true, the MPS server will only accept TLS 1.2 and 1.3 connections. Older Intel AMT devices will not be able to connect."
|
||||||
},
|
},
|
||||||
|
"prometheus": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"number"
|
||||||
|
],
|
||||||
|
"default": false,
|
||||||
|
"description": "When set to true, a prometheus metrics endpoint will be available \"0.0.0.0:9464/metrics\". If you specify a number instead, the prometheus metrics will listen on this port instead of the default 9464."
|
||||||
|
},
|
||||||
"no2FactorAuth": {
|
"no2FactorAuth": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"debug": {
|
||||||
|
"type": "string",
|
||||||
|
"default": null,
|
||||||
|
"description": "You can set this value in the format 'web,agent,relay', where each value is separated by a comma. Each specified value will output logs to the MeshCentral console. Setting it to '*' will output all logs."
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": null
|
"default": null,
|
||||||
|
"description": "You can set this value in the format 'web,agent,relay', where each value is separated by a comma. Each specified value will output logs to a file named 'log.txt' located inside the 'meshcentral-data' folder. Setting it to '*' will output all logs."
|
||||||
},
|
},
|
||||||
"syslog": {
|
"syslog": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -823,7 +864,7 @@
|
||||||
"boolean",
|
"boolean",
|
||||||
"object"
|
"object"
|
||||||
],
|
],
|
||||||
"description": "If set to \"true\", automatic backups of your MeshCentral data will be enabled. Alternatively, you can provide an object with additional values such as \"webdav\", \"backupPath\", \"backupIntervalHours\", and more.",
|
"description": "Enabled by default or if set to true. Disabled if set to false. An object can be provided with additional properties to set autobackup options.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"mongoDumpPath": {
|
"mongoDumpPath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -835,16 +876,31 @@
|
||||||
"default": "mysqldump",
|
"default": "mysqldump",
|
||||||
"description": "The file path of where \"mysqldump\" is located. Default is \"mysqldump\""
|
"description": "The file path of where \"mysqldump\" is located. Default is \"mysqldump\""
|
||||||
},
|
},
|
||||||
|
"pgDumpPath": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "pg_dump",
|
||||||
|
"description": "The file path of where \"pg_dump\" is located. Default is \"pg_dump\""
|
||||||
|
},
|
||||||
"backupIntervalHours": {
|
"backupIntervalHours": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 24,
|
"default": 24,
|
||||||
"description": "How often should the autobackup run in hours from the second meshcentral starts up? Default is every 24 hours"
|
"description": "How often should the autobackup run in hours from the second meshcentral starts up? Default is every 24 hours"
|
||||||
},
|
},
|
||||||
|
"backupHour": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "At which hour the autobackup should run. This forces a daily backup, overrules a custom 'backupIntervalHours'."
|
||||||
|
},
|
||||||
"keepLastDaysBackup": {
|
"keepLastDaysBackup": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 10,
|
"default": 10,
|
||||||
"description": "How many days of backups should the autobackup keep? Default is 10 Days worth"
|
"description": "How many days of backups should the autobackup keep? Default is 10 Days worth"
|
||||||
},
|
},
|
||||||
|
"zipCompression" : {
|
||||||
|
"type": "integer",
|
||||||
|
"default": "5",
|
||||||
|
"description": "Set the zip compression level, 1=fast/less small file to 9=slow/smallest file."
|
||||||
|
},
|
||||||
"zipPassword": {
|
"zipPassword": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"default": "",
|
||||||
|
|
@ -856,6 +912,31 @@
|
||||||
"default": "meshcentral-backups",
|
"default": "meshcentral-backups",
|
||||||
"description": "The file path where backup files are kept. The default is \"meshcentral-backups\" which sits next to \"meshcentral-data\"."
|
"description": "The file path where backup files are kept. The default is \"meshcentral-backups\" which sits next to \"meshcentral-data\"."
|
||||||
},
|
},
|
||||||
|
"backupName": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "meshcentral-autobackup-",
|
||||||
|
"description": "The filename of the backupfile. The default is \"meshcentral-autobackup-\", the filename is appended with the time of backup."
|
||||||
|
},
|
||||||
|
"backupWebFolders": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Add views, public and emails directories if overridden"
|
||||||
|
},
|
||||||
|
"backupOtherFolders": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Also add files and recordings folder to the backup"
|
||||||
|
},
|
||||||
|
"backupIgnoreFilesGlob": {
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"description": "Glob for ignoring files in the data directory. For example [\"**/*.log\"] !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !! Don't do string..."
|
||||||
|
},
|
||||||
|
"backupSkipFoldersGlob":{
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"description": "Glob for ignoring directories in the data directory. For example [\"**/signedagents\"]"
|
||||||
|
},
|
||||||
"googleDrive": {
|
"googleDrive": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Enabled automated upload of the server backups to a Google Drive account, once enabled you need to go in \"My Server\" tab as administrator to associate the account.",
|
"description": "Enabled automated upload of the server backups to a Google Drive account, once enabled you need to go in \"My Server\" tab as administrator to associate the account.",
|
||||||
|
|
@ -1092,6 +1173,11 @@
|
||||||
"default": 2,
|
"default": 2,
|
||||||
"description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages."
|
"description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages."
|
||||||
},
|
},
|
||||||
|
"showModernUIToggle": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "When set to true, the user will be able to toggle between the modern and classic UI."
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "MeshCentral",
|
"default": "MeshCentral",
|
||||||
|
|
@ -1112,6 +1198,11 @@
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "Web site .png logo file placed in meshcentral-data that used on the login page when sitestyle is 2."
|
"description": "Web site .png logo file placed in meshcentral-data that used on the login page when sitestyle is 2."
|
||||||
},
|
},
|
||||||
|
"pwaLogo": {
|
||||||
|
"type": "string",
|
||||||
|
"default": null,
|
||||||
|
"description": "Web site .png logo file that is 512x512 in size placed in meshcentral-data that is used for PWA installs on iOS and Android."
|
||||||
|
},
|
||||||
"rootRedirect": {
|
"rootRedirect": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": null,
|
"default": null,
|
||||||
|
|
@ -1122,6 +1213,11 @@
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "When set to false, this setting will disable the mobile site."
|
"description": "When set to false, this setting will disable the mobile site."
|
||||||
},
|
},
|
||||||
|
"orphanAgentUser": {
|
||||||
|
"type": "string",
|
||||||
|
"default": null,
|
||||||
|
"description": "If an agent attempts to connect to a unknown device group, automatically create a new device group and grant access to the specified user. Example: admin"
|
||||||
|
},
|
||||||
"maxDeviceView": {
|
"maxDeviceView": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": null,
|
"default": null,
|
||||||
|
|
@ -1586,6 +1682,11 @@
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "Set to false to disable SMS 2FA."
|
"description": "Set to false to disable SMS 2FA."
|
||||||
},
|
},
|
||||||
|
"duo2factor": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Set to false to disable Duo 2FA."
|
||||||
|
},
|
||||||
"push2factor": {
|
"push2factor": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
|
|
@ -1870,6 +1971,16 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "If true, user consent is accepted after the timeout."
|
"description": "If true, user consent is accepted after the timeout."
|
||||||
|
},
|
||||||
|
"autoAcceptIfNoUser": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "If true, user consent is accepted if no user is logged in."
|
||||||
|
},
|
||||||
|
"oldStyle": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "If true, user consent will be shown in an old style prompt box rather than the new style consent-box."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2069,6 +2180,11 @@
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "When set, idle users will be disconnected after a set amounts of minutes."
|
"description": "When set, idle users will be disconnected after a set amounts of minutes."
|
||||||
},
|
},
|
||||||
|
"logoutOnIdleSessionTimeout": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Determines whether MeshCentral should logout after the session idle timeout elapsed or should just disconnect remote desktop, terminal and files."
|
||||||
|
},
|
||||||
"userConsentFlags": {
|
"userConsentFlags": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Use this section to require user consent for this domain.",
|
"description": "Use this section to require user consent for this domain.",
|
||||||
|
|
@ -2601,6 +2717,26 @@
|
||||||
},
|
},
|
||||||
"description": "This is used to create HTTP redirections. For example setting \"redirects\": { \"example\":\"https://example.com\" } will make it so that anyone accessing /example on the server will get redirected to the specified URL."
|
"description": "This is used to create HTTP redirections. For example setting \"redirects\": { \"example\":\"https://example.com\" } will make it so that anyone accessing /example on the server will get redirected to the specified URL."
|
||||||
},
|
},
|
||||||
|
"duo2factor": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"integrationkey": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Integration key from Duo"
|
||||||
|
},
|
||||||
|
"secretkey": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Secret key from Duo"
|
||||||
|
},
|
||||||
|
"apihostname": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "API Hostname from Duo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"yubikey": {
|
"yubikey": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -2779,12 +2915,10 @@
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "string",
|
|
||||||
"description": "SMTP username."
|
"description": "SMTP username."
|
||||||
},
|
},
|
||||||
"pass": {
|
"pass": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "string",
|
|
||||||
"description": "SMTP password."
|
"description": "SMTP password."
|
||||||
},
|
},
|
||||||
"tls": {
|
"tls": {
|
||||||
|
|
@ -3137,7 +3271,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"newAccounts": {
|
"newAccounts": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|
@ -3345,8 +3478,7 @@
|
||||||
"required": [
|
"required": [
|
||||||
"client_id",
|
"client_id",
|
||||||
"client_secret"
|
"client_secret"
|
||||||
],
|
]
|
||||||
"additionalProperties": false
|
|
||||||
},
|
},
|
||||||
"issuer": {
|
"issuer": {
|
||||||
"type": [
|
"type": [
|
||||||
|
|
@ -3436,8 +3568,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"additionalProperties": false
|
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -3488,8 +3619,7 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)"
|
"description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"additionalProperties": false
|
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -3541,8 +3671,7 @@
|
||||||
"default": "groups",
|
"default": "groups",
|
||||||
"description": "Custom claim to use."
|
"description": "Custom claim to use."
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3607,8 +3736,7 @@
|
||||||
"description": "EAB HMAC KEY",
|
"description": "EAB HMAC KEY",
|
||||||
"default": ""
|
"default": ""
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -3703,12 +3831,10 @@
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "string",
|
|
||||||
"description": "SMTP username."
|
"description": "SMTP username."
|
||||||
},
|
},
|
||||||
"pass": {
|
"pass": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "string",
|
|
||||||
"description": "SMTP password."
|
"description": "SMTP password."
|
||||||
},
|
},
|
||||||
"tls": {
|
"tls": {
|
||||||
|
|
|
||||||
222
meshcentral.js
222
meshcentral.js
|
|
@ -395,6 +395,92 @@ function CreateMeshCentralServer(config, args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
|
||||||
|
if (obj.platform == 'freebsd') {
|
||||||
|
if (obj.args.install == true) {
|
||||||
|
// Install MeshCentral in rc.d
|
||||||
|
console.log('Installing MeshCentral as background Service...');
|
||||||
|
var systemdConf = "/usr/local/etc/rc.d/meshcentral";
|
||||||
|
const userinfo = require('os').userInfo();
|
||||||
|
console.log('Writing config file...');
|
||||||
|
require('child_process').exec('which node', {}, function (error, stdout, stderr) {
|
||||||
|
if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
|
||||||
|
const nodePath = stdout.substring(0, stdout.indexOf('\n'));
|
||||||
|
const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
|
||||||
|
require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
|
||||||
|
if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
|
||||||
|
console.log('Enabling service...');
|
||||||
|
require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
|
||||||
|
if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
|
||||||
|
if (stdout.length > 0) { console.log(stdout); }
|
||||||
|
console.log('Starting service...');
|
||||||
|
const service = require('child_process').spawn('service', ['meshcentral', 'start']);
|
||||||
|
service.stdout.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.stderr.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.on('exit', function (code) {
|
||||||
|
console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
|
||||||
|
process.exit(); // Must exit otherwise we just hang
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (obj.args.uninstall == true) {
|
||||||
|
// Uninstall MeshCentral in rc.d
|
||||||
|
console.log('Uninstalling MeshCentral background service...');
|
||||||
|
var systemdConf = "/usr/local/etc/rc.d/meshcentral";
|
||||||
|
console.log('Stopping service...');
|
||||||
|
const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
|
||||||
|
service.stdout.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.stderr.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.on('exit', function (code) {
|
||||||
|
if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
|
||||||
|
console.log('Disabling service...');
|
||||||
|
require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
|
||||||
|
if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
|
||||||
|
if (stdout.length > 0) { console.log(stdout); }
|
||||||
|
console.log('Removing config file...');
|
||||||
|
require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
|
||||||
|
if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
|
||||||
|
console.log('Done.');
|
||||||
|
process.exit(); // Must exit otherwise we just hang
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (obj.args.start == true) {
|
||||||
|
// Start MeshCentral in rc.d
|
||||||
|
const service = require('child_process').spawn('service', ['meshcentral', 'start']);
|
||||||
|
service.stdout.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.stderr.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.on('exit', function (code) {
|
||||||
|
console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
|
||||||
|
process.exit(); // Must exit otherwise we just hang
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (obj.args.stop == true) {
|
||||||
|
// Stop MeshCentral in rc.d
|
||||||
|
const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
|
||||||
|
service.stdout.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.stderr.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.on('exit', function (code) {
|
||||||
|
console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
|
||||||
|
process.exit(); // Must exit otherwise we just hang
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (obj.args.restart == true) {
|
||||||
|
// Restart MeshCentral in rc.d
|
||||||
|
const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
|
||||||
|
service.stdout.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.stderr.on('data', function (data) { console.log(data.toString()); });
|
||||||
|
service.on('exit', function (code) {
|
||||||
|
console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
|
||||||
|
process.exit(); // Must exit otherwise we just hang
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Index a recorded file
|
// Index a recorded file
|
||||||
if (obj.args.indexmcrec != null) {
|
if (obj.args.indexmcrec != null) {
|
||||||
if (typeof obj.args.indexmcrec != 'string') {
|
if (typeof obj.args.indexmcrec != 'string') {
|
||||||
|
|
@ -497,8 +583,11 @@ function CreateMeshCentralServer(config, args) {
|
||||||
// Launch MeshCentral as a child server and monitor it.
|
// Launch MeshCentral as a child server and monitor it.
|
||||||
obj.launchChildServer = function (startArgs) {
|
obj.launchChildServer = function (startArgs) {
|
||||||
const child_process = require('child_process');
|
const child_process = require('child_process');
|
||||||
|
const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
|
||||||
|
const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
|
||||||
try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
|
try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
|
||||||
try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
|
try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
|
||||||
|
if (startArgs[0] != "--disable-proto=delete") startArgs.unshift("--disable-proto=delete")
|
||||||
childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
|
childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
|
||||||
if (childProcess.xrestart == 1) {
|
if (childProcess.xrestart == 1) {
|
||||||
setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
|
setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
|
||||||
|
|
@ -570,12 +659,12 @@ function CreateMeshCentralServer(config, args) {
|
||||||
else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
|
else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
|
||||||
var datastr = data;
|
var datastr = data;
|
||||||
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
|
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
|
||||||
console.log(datastr);
|
logFromChildProcess(datastr);
|
||||||
});
|
});
|
||||||
childProcess.stderr.on('data', function (data) {
|
childProcess.stderr.on('data', function (data) {
|
||||||
var datastr = data;
|
var datastr = data;
|
||||||
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
|
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
|
||||||
console.log('ERR: ' + datastr);
|
logFromChildProcess('ERR: ' + datastr);
|
||||||
if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
|
if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
|
||||||
if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
|
if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
|
||||||
obj.logError(data);
|
obj.logError(data);
|
||||||
|
|
@ -1262,7 +1351,7 @@ function CreateMeshCentralServer(config, args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the database is capable of performing a backup
|
// Check if the database is capable of performing a backup
|
||||||
obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
|
// Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
|
||||||
|
|
||||||
// Load configuration for database if needed
|
// Load configuration for database if needed
|
||||||
if (obj.args.loadconfigfromdb) {
|
if (obj.args.loadconfigfromdb) {
|
||||||
|
|
@ -1570,7 +1659,7 @@ function CreateMeshCentralServer(config, args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup agent error log
|
// Setup agent error log
|
||||||
if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump != null)) {
|
if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
|
||||||
obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
|
obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1906,9 +1995,17 @@ function CreateMeshCentralServer(config, args) {
|
||||||
if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
|
if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current node version
|
||||||
|
const verSplit = process.version.substring(1).split('.');
|
||||||
|
var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
|
||||||
|
|
||||||
// Setup Firebase
|
// Setup Firebase
|
||||||
if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
|
if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
|
||||||
obj.firebase = require('./firebase').CreateFirebase(obj, config.firebase.senderid, config.firebase.serverkey);
|
addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
|
||||||
|
} else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
|
||||||
|
var serviceAccount;
|
||||||
|
try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
|
||||||
|
if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
|
||||||
} else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
|
} else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
|
||||||
// Setup the push messaging relay
|
// Setup the push messaging relay
|
||||||
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
|
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
|
||||||
|
|
@ -1917,8 +2014,12 @@ function CreateMeshCentralServer(config, args) {
|
||||||
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
|
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup monitoring
|
||||||
|
obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args);
|
||||||
|
|
||||||
// Start periodic maintenance
|
// Start periodic maintenance
|
||||||
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
|
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
|
||||||
|
//obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
|
||||||
|
|
||||||
// Dispatch an event that the server is now running
|
// Dispatch an event that the server is now running
|
||||||
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
|
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
|
||||||
|
|
@ -2003,18 +2104,24 @@ function CreateMeshCentralServer(config, args) {
|
||||||
obj.updateServerState('state', "running");
|
obj.updateServerState('state', "running");
|
||||||
|
|
||||||
// Setup auto-backup defaults
|
// Setup auto-backup defaults
|
||||||
if (obj.config.settings.autobackup == null || obj.config.settings.autobackup === true) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; }
|
if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: 0}; } //no schedule, but able to console autobackup
|
||||||
else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; }
|
else {
|
||||||
else if (typeof obj.config.settings.autobackup == 'object'){
|
if (obj.config.settings.autobackup == null || obj.config.settings.autobackup === true) { obj.config.settings.autobackup = {backupintervalhours: 24, keeplastdaysbackup: 10}; };
|
||||||
if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; }
|
if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
|
||||||
if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; }
|
if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
|
||||||
|
if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
|
||||||
|
else {obj.config.settings.autobackup.backuphour = -1 };
|
||||||
|
//arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
|
||||||
|
if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
|
||||||
|
else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
|
||||||
|
if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
|
||||||
|
else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
|
||||||
|
if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
|
||||||
|
if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that autobackup path is not within the "meshcentral-data" folder.
|
// Check if the database is capable of performing a backup
|
||||||
if ((typeof obj.config.settings.autobackup == 'object') && (typeof obj.config.settings.autobackup.backuppath == 'string') && (obj.path.normalize(obj.config.settings.autobackup.backuppath).startsWith(obj.path.normalize(obj.datapath)))) {
|
obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
|
||||||
addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21);
|
|
||||||
delete obj.config.settings.autobackup;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Intel AMT passwords from the "amtactivation.log" file
|
// Load Intel AMT passwords from the "amtactivation.log" file
|
||||||
obj.loadAmtActivationLogPasswords(function (amtPasswords) {
|
obj.loadAmtActivationLogPasswords(function (amtPasswords) {
|
||||||
|
|
@ -2176,14 +2283,19 @@ function CreateMeshCentralServer(config, args) {
|
||||||
|
|
||||||
// Check if we need to perform an automatic backup
|
// Check if we need to perform an automatic backup
|
||||||
function checkAutobackup() {
|
function checkAutobackup() {
|
||||||
if (obj.config.settings.autobackup && (typeof obj.config.settings.autobackup.backupintervalhours == 'number')) {
|
if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
|
||||||
obj.db.Get('LastAutoBackupTime', function (err, docs) {
|
obj.db.Get('LastAutoBackupTime', function (err, docs) {
|
||||||
if (err != null) return;
|
if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
|
||||||
var lastBackup = 0;
|
var lastBackup = 0;
|
||||||
const now = new Date().getTime();
|
const currentdate = new Date();
|
||||||
|
let currentHour = currentdate.getHours();
|
||||||
|
let now = currentdate.getTime();
|
||||||
if (docs.length == 1) { lastBackup = docs[0].value; }
|
if (docs.length == 1) { lastBackup = docs[0].value; }
|
||||||
const delta = now - lastBackup;
|
const delta = now - lastBackup;
|
||||||
if (delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) {
|
//const delta = 9999999999; // DEBUG: backup always
|
||||||
|
obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
|
||||||
|
//start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
|
||||||
|
if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
|
||||||
// A new auto-backup is required.
|
// A new auto-backup is required.
|
||||||
obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
|
obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
|
||||||
obj.db.performBackup(); // Perform the backup
|
obj.db.performBackup(); // Perform the backup
|
||||||
|
|
@ -2305,6 +2417,10 @@ function CreateMeshCentralServer(config, args) {
|
||||||
storeEvent.links = Object.assign({}, storeEvent.links);
|
storeEvent.links = Object.assign({}, storeEvent.links);
|
||||||
for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
|
for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
|
||||||
}
|
}
|
||||||
|
if (storeEvent.mesh) {
|
||||||
|
// Escape "mesh" names that may have "." and/or "$"
|
||||||
|
storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
|
||||||
|
}
|
||||||
storeEvent.ids = ids;
|
storeEvent.ids = ids;
|
||||||
obj.db.StoreEvent(storeEvent);
|
obj.db.StoreEvent(storeEvent);
|
||||||
}
|
}
|
||||||
|
|
@ -3138,13 +3254,13 @@ function CreateMeshCentralServer(config, args) {
|
||||||
|
|
||||||
// Setup the time server
|
// Setup the time server
|
||||||
var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
|
var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
|
||||||
if (args.agenttimestampserver === false) { timeStampUrl = null; }
|
if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
|
||||||
else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; }
|
else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
|
||||||
|
|
||||||
// Setup the time server proxy
|
// Setup the time server proxy
|
||||||
var timeStampProxy = null;
|
var timeStampProxy = null;
|
||||||
if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; }
|
if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
|
||||||
else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; }
|
else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
|
||||||
|
|
||||||
// Setup the pending operations counter
|
// Setup the pending operations counter
|
||||||
var pendingOperations = 1;
|
var pendingOperations = 1;
|
||||||
|
|
@ -3760,7 +3876,7 @@ function CreateMeshCentralServer(config, args) {
|
||||||
// Send event to log file
|
// Send event to log file
|
||||||
if (obj.config.settings && obj.config.settings.log) {
|
if (obj.config.settings && obj.config.settings.log) {
|
||||||
if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
|
if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
|
||||||
if (obj.args.log.indexOf(source) >= 0) {
|
if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
if (obj.xxLogFile == null) {
|
if (obj.xxLogFile == null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3772,7 +3888,8 @@ function CreateMeshCentralServer(config, args) {
|
||||||
if (obj.xxLogFile != null) {
|
if (obj.xxLogFile != null) {
|
||||||
try {
|
try {
|
||||||
if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
|
if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
|
||||||
obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + args.join(', ') + '\r\n');
|
const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
|
||||||
|
obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
|
||||||
} catch (ex) { }
|
} catch (ex) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3829,6 +3946,7 @@ function CreateMeshCentralServer(config, args) {
|
||||||
function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
|
function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
|
||||||
function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
|
function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
|
||||||
obj.getServerWarnings = function () { return serverWarnings; }
|
obj.getServerWarnings = function () { return serverWarnings; }
|
||||||
|
// TODO: migrate from other addServerWarning function and add timestamp
|
||||||
obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
|
obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
|
||||||
|
|
||||||
// auth.log functions
|
// auth.log functions
|
||||||
|
|
@ -3941,7 +4059,16 @@ function InstallModules(modules, args, func) {
|
||||||
try {
|
try {
|
||||||
// Does the module need a specific version?
|
// Does the module need a specific version?
|
||||||
if (moduleVersion) {
|
if (moduleVersion) {
|
||||||
if (require(`${moduleName}/package.json`).version != moduleVersion) { throw new Error(); }
|
var versionMatch = false;
|
||||||
|
var modulePath = null;
|
||||||
|
// This is the first way to test if a module is already installed.
|
||||||
|
try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
|
||||||
|
if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
|
||||||
|
}
|
||||||
|
// If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
|
||||||
|
if ((versionMatch == false) && (modulePath != null)) {
|
||||||
|
if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// For all other modules, do the check here.
|
// For all other modules, do the check here.
|
||||||
// Is the module in package.json? Install exact version.
|
// Is the module in package.json? Install exact version.
|
||||||
|
|
@ -3990,6 +4117,7 @@ function InstallModuleEx(modulenames, args, func) {
|
||||||
process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
|
process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
|
||||||
|
|
||||||
// Add a server warning, warnings will be shown to the administrator on the web application
|
// Add a server warning, warnings will be shown to the administrator on the web application
|
||||||
|
// TODO: migrate to obj.addServerWarning?
|
||||||
const serverWarnings = [];
|
const serverWarnings = [];
|
||||||
function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
|
function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
|
||||||
|
|
||||||
|
|
@ -4020,7 +4148,8 @@ var ServerWarnings = {
|
||||||
23: "Unable to load agent icon file: {0}.",
|
23: "Unable to load agent icon file: {0}.",
|
||||||
24: "Unable to load agent logo file: {0}.",
|
24: "Unable to load agent logo file: {0}.",
|
||||||
25: "This NodeJS version does not support OpenID.",
|
25: "This NodeJS version does not support OpenID.",
|
||||||
26: "This NodeJS version does not support Discord.js."
|
26: "This NodeJS version does not support Discord.js.",
|
||||||
|
27: "Firebase now requires a service account JSON file, Firebase disabled."
|
||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -4074,7 +4203,7 @@ function mainStart() {
|
||||||
// Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
|
// Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
|
||||||
var sspi = false;
|
var sspi = false;
|
||||||
var ldap = false;
|
var ldap = false;
|
||||||
var passport = null;
|
var passport = [];
|
||||||
var allsspi = true;
|
var allsspi = true;
|
||||||
var yubikey = false;
|
var yubikey = false;
|
||||||
var ssh = false;
|
var ssh = false;
|
||||||
|
|
@ -4095,17 +4224,17 @@ function mainStart() {
|
||||||
if (mstsc == false) { config.domains[i].mstsc = false; }
|
if (mstsc == false) { config.domains[i].mstsc = false; }
|
||||||
if (config.domains[i].ssh == true) { ssh = true; }
|
if (config.domains[i].ssh == true) { ssh = true; }
|
||||||
if ((typeof config.domains[i].authstrategies == 'object')) {
|
if ((typeof config.domains[i].authstrategies == 'object')) {
|
||||||
if (passport == null) { passport = ['passport','connect-flash']; } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
|
if (passport.indexOf('passport') == -1) { passport.push('passport','connect-flash'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
|
||||||
if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
|
if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
|
||||||
if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
|
if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
|
||||||
if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
|
if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
|
||||||
if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
|
if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
|
||||||
if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client') == -1)) {
|
if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
|
||||||
if ((nodeVersion >= 17)
|
if ((nodeVersion >= 17)
|
||||||
|| ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
|
|| ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
|
||||||
|| ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
|
|| ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
|
||||||
|| ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
|
|| ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
|
||||||
passport.push('openid-client');
|
passport.push('openid-client@5.7.1');
|
||||||
} else {
|
} else {
|
||||||
addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
|
addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
|
||||||
delete config.domains[i].authstrategies.oidc;
|
delete config.domains[i].authstrategies.oidc;
|
||||||
|
|
@ -4116,47 +4245,49 @@ function mainStart() {
|
||||||
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
|
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
|
||||||
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
|
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
|
||||||
if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
|
if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
|
||||||
|
if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the list of required modules
|
// Build the list of required modules
|
||||||
// NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
|
// NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
|
||||||
var modules = ['archiver@7.0.0', 'body-parser@1.20.2', 'cbor@5.2.0', 'compression@1.7.4', 'cookie-session@2.0.0', 'express@4.19.2', 'express-handlebars@5.3.5', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@yetzt/nedb', 'node-forge@1.3.1', 'ua-parser-js@1.0.37', 'ws@8.17.1', 'yauzl@2.10.0'];
|
var modules = ['archiver@7.0.1', 'body-parser@1.20.3', 'cbor@5.2.0', 'compression@1.7.5', 'cookie-session@2.1.0', 'express@4.21.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb', 'node-forge@1.3.1', 'ua-parser-js@1.0.39', 'ws@8.18.0', 'yauzl@2.10.0'];
|
||||||
if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
|
if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
|
||||||
if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
|
if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
|
||||||
if (ssh == true) { modules.push('ssh2@1.15.0'); }
|
if (ssh == true) { modules.push('ssh2@1.16.0'); }
|
||||||
if (passport != null) { modules.push(...passport); }
|
if (passport != null) { modules.push(...passport); }
|
||||||
if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
|
if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
|
||||||
|
|
||||||
if (sessionRecording == true) { modules.push('image-size@1.0.2'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
|
if (sessionRecording == true) { modules.push('image-size@1.1.1'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
|
||||||
if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
|
if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
|
||||||
if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
|
if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
|
||||||
if (config.settings.mysql != null) { modules.push('mysql2@3.6.2'); } // Add MySQL.
|
if (config.settings.mysql != null) { modules.push('mysql2@3.11.4'); } // Add MySQL.
|
||||||
//if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
|
//if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
|
||||||
if (config.settings.mongodb != null) { modules.push('mongodb@4.13.0'); modules.push('saslprep@1.0.3'); } // Add MongoDB, official driver.
|
if (config.settings.mongodb != null) { modules.push('mongodb@4.13.0'); modules.push('saslprep@1.0.3'); } // Add MongoDB, official driver.
|
||||||
if (config.settings.postgres != null) { modules.push('pg@8.7.1'); modules.push('pgtools@0.3.2'); } // Add Postgres, Postgres driver.
|
if (config.settings.postgres != null) { modules.push('pg@8.13.1') } // Add Postgres, official driver.
|
||||||
if (config.settings.mariadb != null) { modules.push('mariadb@3.2.2'); } // Add MariaDB, official driver.
|
if (config.settings.mariadb != null) { modules.push('mariadb@3.4.0'); } // Add MariaDB, official driver.
|
||||||
if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
|
if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
|
||||||
if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.6'); } // Add sqlite3, official driver.
|
if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
|
||||||
if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
|
if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
|
||||||
if (config.settings.plugins != null) { modules.push('semver@7.5.4'); } // Required for version compat testing and update checks
|
if (config.settings.plugins != null) { modules.push('semver@7.5.4'); } // Required for version compat testing and update checks
|
||||||
if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
|
if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
|
||||||
else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
|
else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
|
||||||
if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.9.8'); } // Add SMTP support
|
if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.9.16'); } // Add SMTP support
|
||||||
if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support
|
if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support
|
||||||
if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('minify-js@0.0.4'); modules.push('html-minifier@4.0.0'); } // Translation support
|
if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('html-minifier@4.0.0'); } // Translation support
|
||||||
if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
|
if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
|
||||||
|
if (config.settings.prometheus != null) { modules.push('prom-client'); } // Add Prometheus Metrics support
|
||||||
|
|
||||||
if (typeof config.settings.autobackup == 'object') {
|
if (typeof config.settings.autobackup == 'object') {
|
||||||
// Setup encrypted zip support if needed
|
// Setup encrypted zip support if needed
|
||||||
if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@1.0.11'); }
|
if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
|
||||||
// Enable Google Drive Support
|
// Enable Google Drive Support
|
||||||
if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
|
if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
|
||||||
// Enable WebDAV Support
|
// Enable WebDAV Support
|
||||||
if (typeof config.settings.autobackup.webdav == 'object') {
|
if (typeof config.settings.autobackup.webdav == 'object') {
|
||||||
if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@4.11.3'); }
|
if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@4.11.4'); }
|
||||||
}
|
}
|
||||||
// Enable S3 Support
|
// Enable S3 Support
|
||||||
if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.1'); }
|
if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.2'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup common password blocking
|
// Setup common password blocking
|
||||||
|
|
@ -4170,7 +4301,7 @@ function mainStart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop multiplexor support
|
// Desktop multiplexor support
|
||||||
if (config.settings.desktopmultiplex === true) { modules.push('image-size@1.0.2'); }
|
if (config.settings.desktopmultiplex === true) { modules.push('image-size@1.1.1'); }
|
||||||
|
|
||||||
// SMS support
|
// SMS support
|
||||||
if (config.sms != null) {
|
if (config.sms != null) {
|
||||||
|
|
@ -4192,8 +4323,7 @@ function mainStart() {
|
||||||
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
|
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
|
||||||
|
|
||||||
// Firebase Support
|
// Firebase Support
|
||||||
// Avoid 0.1.8 due to bugs: https://github.com/guness/node-xcs/issues/43
|
if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
|
||||||
if (config.firebase != null) { modules.push('node-xcs@0.1.7'); }
|
|
||||||
|
|
||||||
// Syslog support
|
// Syslog support
|
||||||
if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
|
if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
|
||||||
|
|
|
||||||
145
meshctrl.js
145
meshctrl.js
|
|
@ -16,7 +16,7 @@ var settings = {};
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const args = require('minimist')(process.argv.slice(2));
|
const args = require('minimist')(process.argv.slice(2));
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage'];
|
const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
|
||||||
if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
|
if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
|
||||||
|
|
||||||
if (args['_'].length == 0) {
|
if (args['_'].length == 0) {
|
||||||
|
|
@ -36,6 +36,8 @@ if (args['_'].length == 0) {
|
||||||
console.log(" ListEvents - List server events.");
|
console.log(" ListEvents - List server events.");
|
||||||
console.log(" LoginTokens - List, create and remove login tokens.");
|
console.log(" LoginTokens - List, create and remove login tokens.");
|
||||||
console.log(" DeviceInfo - Show information about a device.");
|
console.log(" DeviceInfo - Show information about a device.");
|
||||||
|
console.log(" AddLocalDevice - Add a local device.");
|
||||||
|
console.log(" AddAmtDevice - Add a AMT device.");
|
||||||
console.log(" EditDevice - Make changes to a device.");
|
console.log(" EditDevice - Make changes to a device.");
|
||||||
console.log(" RemoveDevice - Delete a device.");
|
console.log(" RemoveDevice - Delete a device.");
|
||||||
console.log(" Config - Perform operation on config.json file.");
|
console.log(" Config - Perform operation on config.json file.");
|
||||||
|
|
@ -63,6 +65,7 @@ if (args['_'].length == 0) {
|
||||||
console.log(" Shell - Access command shell of a remote device.");
|
console.log(" Shell - Access command shell of a remote device.");
|
||||||
console.log(" Upload - Upload a file to a remote device.");
|
console.log(" Upload - Upload a file to a remote device.");
|
||||||
console.log(" Download - Download a file from a remote device.");
|
console.log(" Download - Download a file from a remote device.");
|
||||||
|
console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device.");
|
||||||
console.log(" DeviceOpenUrl - Open a URL on a remote device.");
|
console.log(" DeviceOpenUrl - Open a URL on a remote device.");
|
||||||
console.log(" DeviceMessage - Open a message box on a remote device.");
|
console.log(" DeviceMessage - Open a message box on a remote device.");
|
||||||
console.log(" DeviceToast - Display a toast notification on a remote device.");
|
console.log(" DeviceToast - Display a toast notification on a remote device.");
|
||||||
|
|
@ -109,6 +112,22 @@ if (args['_'].length == 0) {
|
||||||
else { ok = true; }
|
else { ok = true; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'addlocaldevice': {
|
||||||
|
if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
|
||||||
|
else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
|
||||||
|
else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
|
||||||
|
else { ok = true; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'addamtdevice': {
|
||||||
|
if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
|
||||||
|
else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
|
||||||
|
else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
|
||||||
|
else if (args.user == null) { console.log(winRemoveSingleQuotes("Missing user, use --user [user]")); }
|
||||||
|
else if (args.pass == null) { console.log(winRemoveSingleQuotes("Missing pass, use --pass [pass]")); }
|
||||||
|
else { ok = true; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'addusertodevicegroup': {
|
case 'addusertodevicegroup': {
|
||||||
if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
|
if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
|
||||||
else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
|
else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
|
||||||
|
|
@ -259,6 +278,12 @@ if (args['_'].length == 0) {
|
||||||
else { ok = true; }
|
else { ok = true; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'webrelay': {
|
||||||
|
if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
|
||||||
|
else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); }
|
||||||
|
else { ok = true; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'deviceopenurl': {
|
case 'deviceopenurl': {
|
||||||
if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
|
if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
|
||||||
else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
|
else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
|
||||||
|
|
@ -788,6 +813,55 @@ if (args['_'].length == 0) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'addlocaldevice': {
|
||||||
|
console.log("Add a Local Device, Example usages:\r\n");
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname'"));
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname' --type 6"));
|
||||||
|
console.log("\r\nRequired arguments:\r\n");
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
console.log(" --id [meshid] - The mesh identifier.");
|
||||||
|
console.log(" --devicename [devicename] - The device name.");
|
||||||
|
console.log(" --hostname [hostname] - The devices hostname or ip address.");
|
||||||
|
} else {
|
||||||
|
console.log(" --id '[meshid]' - The mesh identifier.");
|
||||||
|
console.log(" --devicename '[devicename]' - The device name.");
|
||||||
|
console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\r\nOptional arguments:\r\n");
|
||||||
|
console.log(" --type [TypeNumber] - With the following choices:");
|
||||||
|
console.log(" type 4 - Default, Windows (RDP)");
|
||||||
|
console.log(" type 6 - Linux (SSH/SCP/VNC)");
|
||||||
|
console.log(" type 29 - macOS (SSH/SCP/VNC)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'addamtdevice': {
|
||||||
|
console.log("Add an Intel AMT Device, Example usages:\r\n");
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin'"));
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin' --notls"));
|
||||||
|
console.log("\r\nRequired arguments:\r\n");
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
console.log(" --id [meshid] - The mesh identifier.");
|
||||||
|
console.log(" --devicename [devicename] - The device name.");
|
||||||
|
console.log(" --hostname [hostname] - The devices hostname or ip address.");
|
||||||
|
console.log(" --user [user] - The devices AMT username.");
|
||||||
|
console.log(" --pass [pass] - The devices AMT password.");
|
||||||
|
console.log("")
|
||||||
|
} else {
|
||||||
|
console.log(" --id '[meshid]' - The mesh identifier.");
|
||||||
|
console.log(" --devicename '[devicename]' - The device name.");
|
||||||
|
console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
|
||||||
|
console.log(" --user '[user]' - The devices AMT username.");
|
||||||
|
console.log(" --pass '[pass]' - The devices AMT password.");
|
||||||
|
}
|
||||||
|
console.log("\r\nOptional arguments:\r\n");
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
console.log(" --notls - Use No TLS Security.");
|
||||||
|
} else {
|
||||||
|
console.log(" --notls - Use No TLS Security.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'editdevice': {
|
case 'editdevice': {
|
||||||
console.log("Change information about a device, Example usages:\r\n");
|
console.log("Change information about a device, Example usages:\r\n");
|
||||||
console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
|
console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
|
||||||
|
|
@ -948,6 +1022,21 @@ if (args['_'].length == 0) {
|
||||||
console.log(" --target [localpath] - The local path to download the file to.");
|
console.log(" --target [localpath] - The local path to download the file to.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'webrelay': {
|
||||||
|
console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n");
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80"));
|
||||||
|
console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443"));
|
||||||
|
console.log("\r\nRequired arguments:\r\n");
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
console.log(" --id [deviceid] - The device identifier.");
|
||||||
|
} else {
|
||||||
|
console.log(" --id '[deviceid]' - The device identifier.");
|
||||||
|
}
|
||||||
|
console.log(" --type [http,https] - Type of relay from remote device, http or https.");
|
||||||
|
console.log("\r\nOptional arguments:\r\n");
|
||||||
|
console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'deviceopenurl': {
|
case 'deviceopenurl': {
|
||||||
console.log("Open a web page on a remote device, Example usages:\r\n");
|
console.log("Open a web page on a remote device, Example usages:\r\n");
|
||||||
console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
|
console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
|
||||||
|
|
@ -1490,6 +1579,29 @@ function serverConnect() {
|
||||||
ws.send(JSON.stringify(op));
|
ws.send(JSON.stringify(op));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'addamtdevice': {
|
||||||
|
var op = { action: 'addamtdevice', amttls: 1, responseid: 'meshctrl' };
|
||||||
|
if (args.id) { op.meshid = args.id; }
|
||||||
|
if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
|
||||||
|
if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
|
||||||
|
if ((typeof args.user == 'string') && (args.user != '')) { op.amtusername = args.user; }
|
||||||
|
if ((typeof args.pass == 'string') && (args.pass != '')) { op.amtpassword = args.pass; }
|
||||||
|
if (args.notls) { op.amttls = 0; }
|
||||||
|
ws.send(JSON.stringify(op));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'addlocaldevice': {
|
||||||
|
var op = { action: 'addlocaldevice', type: 4, responseid: 'meshctrl' };
|
||||||
|
if (args.id) { op.meshid = args.id; }
|
||||||
|
if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
|
||||||
|
if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
|
||||||
|
if (args.type) {
|
||||||
|
if ((typeof parseInt(args.type) != 'number') || isNaN(parseInt(args.type))) { console.log("Invalid type."); process.exit(1); return; }
|
||||||
|
op.type = args.type;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify(op));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'editdevicegroup': {
|
case 'editdevicegroup': {
|
||||||
var op = { action: 'editmesh', responseid: 'meshctrl' };
|
var op = { action: 'editmesh', responseid: 'meshctrl' };
|
||||||
if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
|
if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
|
||||||
|
|
@ -1714,6 +1826,29 @@ function serverConnect() {
|
||||||
req.end()
|
req.end()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'webrelay': {
|
||||||
|
var protocol = null;
|
||||||
|
if (args.type != null) {
|
||||||
|
if (args.type == 'http') {
|
||||||
|
protocol = 1;
|
||||||
|
} else if (args.type == 'https') {
|
||||||
|
protocol = 2;
|
||||||
|
} else {
|
||||||
|
console.log("Unknown protocol type: " + args.type); process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var port = null;
|
||||||
|
if (typeof args.port == 'number') {
|
||||||
|
if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
|
||||||
|
port = args.port;
|
||||||
|
} else if (protocol == 1) {
|
||||||
|
port = 80;
|
||||||
|
} else if (protocol == 2) {
|
||||||
|
port = 443;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'devicesharing': {
|
case 'devicesharing': {
|
||||||
if (args.add) {
|
if (args.add) {
|
||||||
if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
|
if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
|
||||||
|
|
@ -2084,6 +2219,8 @@ function serverConnect() {
|
||||||
case 'toast': // TOAST
|
case 'toast': // TOAST
|
||||||
case 'adduser': // ADDUSER
|
case 'adduser': // ADDUSER
|
||||||
case 'edituser': // EDITUSER
|
case 'edituser': // EDITUSER
|
||||||
|
case 'addamtdevice': // ADDAMTDEVICE
|
||||||
|
case 'addlocaldevice': // ADDLOCALDEVICE
|
||||||
case 'removedevices': // REMOVEDEVICE
|
case 'removedevices': // REMOVEDEVICE
|
||||||
case 'changedevice': // EDITDEVICE
|
case 'changedevice': // EDITDEVICE
|
||||||
case 'deleteuser': // REMOVEUSER
|
case 'deleteuser': // REMOVEUSER
|
||||||
|
|
@ -2106,6 +2243,7 @@ function serverConnect() {
|
||||||
case 'removeDeviceShare':
|
case 'removeDeviceShare':
|
||||||
case 'userbroadcast': { // BROADCAST
|
case 'userbroadcast': { // BROADCAST
|
||||||
if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) return;
|
if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) return;
|
||||||
|
if ((data.type == 'runcommands') && (settings.cmd != 'runcommand')) return;
|
||||||
if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
|
if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
|
||||||
if (data.responseid == 'meshctrl') {
|
if (data.responseid == 'meshctrl') {
|
||||||
if (data.meshid) { console.log(data.result, data.meshid); }
|
if (data.meshid) { console.log(data.result, data.meshid); }
|
||||||
|
|
@ -2116,6 +2254,7 @@ function serverConnect() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'createDeviceShareLink':
|
case 'createDeviceShareLink':
|
||||||
|
case 'webrelay':
|
||||||
if (data.result == 'OK') {
|
if (data.result == 'OK') {
|
||||||
if (data.publicid) { console.log('ID: ' + data.publicid); }
|
if (data.publicid) { console.log('ID: ' + data.publicid); }
|
||||||
console.log('URL: ' + data.url);
|
console.log('URL: ' + data.url);
|
||||||
|
|
@ -2527,8 +2666,8 @@ function getDevicesThatMatchFilter(nodes, x) {
|
||||||
} else if (tagSearch != null) {
|
} else if (tagSearch != null) {
|
||||||
// Tag filter
|
// Tag filter
|
||||||
for (var d in nodes) {
|
for (var d in nodes) {
|
||||||
if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(d); }
|
if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(nodes[d]); }
|
||||||
else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(d); break; } } }
|
else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(nodes[d]); break; } } }
|
||||||
}
|
}
|
||||||
} else if (agentTagSearch != null) {
|
} else if (agentTagSearch != null) {
|
||||||
// Agent Tag filter
|
// Agent Tag filter
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, id, func) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Write the recording file header
|
// Write the recording file header
|
||||||
parent.parent.debug('relay', 'Relay: Started recoding to file: ' + recFullFilename);
|
parent.parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename);
|
||||||
var metadata = { magic: 'MeshCentralRelaySession', ver: 1, nodeid: obj.nodeid, meshid: obj.meshid, time: new Date().toLocaleString(), protocol: 2, devicename: obj.name, devicegroup: obj.meshname };
|
var metadata = { magic: 'MeshCentralRelaySession', ver: 1, nodeid: obj.nodeid, meshid: obj.meshid, time: new Date().toLocaleString(), protocol: 2, devicename: obj.name, devicegroup: obj.meshname };
|
||||||
var firstBlock = JSON.stringify(metadata);
|
var firstBlock = JSON.stringify(metadata);
|
||||||
recordingEntry(fd, 1, 0, firstBlock, function () {
|
recordingEntry(fd, 1, 0, firstBlock, function () {
|
||||||
|
|
@ -1347,6 +1347,8 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) {
|
||||||
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
||||||
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
||||||
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
||||||
|
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
|
||||||
|
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
|
||||||
}
|
}
|
||||||
if (typeof domain.notificationmessages == 'object') {
|
if (typeof domain.notificationmessages == 'object') {
|
||||||
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
||||||
|
|
|
||||||
|
|
@ -352,9 +352,9 @@ module.exports.CreateServer = function (parent) {
|
||||||
push.send(domain.title ? domain.title : 'MeshCentral', msg, function (err, res) { if (func != null) { func(err == null); } });
|
push.send(domain.title ? domain.title : 'MeshCentral', msg, function (err, res) { if (func != null) { func(err == null); } });
|
||||||
} else if ((to.startsWith('ntfy:')) && (obj.ntfyClient != null)) { // ntfy
|
} else if ((to.startsWith('ntfy:')) && (obj.ntfyClient != null)) { // ntfy
|
||||||
const url = 'https://' + (((typeof parent.config.messaging.ntfy == 'object') && (typeof parent.config.messaging.ntfy.host == 'string')) ? parent.config.messaging.ntfy.host : 'ntfy.sh') + '/' + encodeURIComponent(to.substring(5));
|
const url = 'https://' + (((typeof parent.config.messaging.ntfy == 'object') && (typeof parent.config.messaging.ntfy.host == 'string')) ? parent.config.messaging.ntfy.host : 'ntfy.sh') + '/' + encodeURIComponent(to.substring(5));
|
||||||
const headers = (typeof parent.config.messaging.ntfy.authorization == 'string') ? { 'Authorization': parent.config.messaging.ntfy.authorization } : {};
|
const headers = { 'User-Agent': 'MeshCentral v' + parent.currentVer };
|
||||||
const req = require('https').request(new URL(url), { method: 'POST', headers: headers }, function (res) { if (func != null) { func(true); } });
|
if (typeof parent.config.messaging.ntfy.authorization == 'string') { headers['Authorization'] = parent.config.messaging.ntfy.authorization; }
|
||||||
req.on('error', function (err) { if (func != null) { func(false); } });
|
const req = require('https').request(new URL(url), { method: 'POST', headers: headers }, function (res) { if (func != null) { func(res.statusCode == 200); } });
|
||||||
req.end(msg);
|
req.end(msg);
|
||||||
} else if ((to.startsWith('zulip:')) && (obj.zulipClient != null)) { // zulip
|
} else if ((to.startsWith('zulip:')) && (obj.zulipClient != null)) { // zulip
|
||||||
obj.zulipClient.sendMessage({
|
obj.zulipClient.sendMessage({
|
||||||
|
|
|
||||||
26
meshrelay.js
26
meshrelay.js
|
|
@ -120,6 +120,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if ((typeof sr == 'number') && (sr > 0) && (sr < 1000)) { obj.ws.slowRelay = sr; }
|
if ((typeof sr == 'number') && (sr > 0) && (sr < 1000)) { obj.ws.slowRelay = sr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if protocol is set in the cookie and if so replace req.query.p but only if its not already set or blank
|
||||||
|
if ((cookie != null) && (typeof cookie.p == 'number') && (obj.req.query.p === undefined || obj.req.query.p === "")) { obj.req.query.p = cookie.p; }
|
||||||
|
|
||||||
// Mesh Rights
|
// Mesh Rights
|
||||||
const MESHRIGHT_EDITMESH = 1;
|
const MESHRIGHT_EDITMESH = 1;
|
||||||
const MESHRIGHT_MANAGEUSERS = 2;
|
const MESHRIGHT_MANAGEUSERS = 2;
|
||||||
|
|
@ -442,15 +445,15 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
relayinfo.peer1.sendPeerImage();
|
relayinfo.peer1.sendPeerImage();
|
||||||
} else {
|
} else {
|
||||||
// Write the recording file header
|
// Write the recording file header
|
||||||
parent.parent.debug('relay', 'Relay: Started recoding to file: ' + recFullFilename);
|
parent.parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename);
|
||||||
var metadata = {
|
var metadata = {
|
||||||
magic: 'MeshCentralRelaySession',
|
magic: 'MeshCentralRelaySession',
|
||||||
ver: 1,
|
ver: 1,
|
||||||
userid: sessionUser._id,
|
userid: sessionUser._id,
|
||||||
username: sessionUser.name,
|
username: sessionUser.name,
|
||||||
sessionid: obj.id,
|
sessionid: obj.id,
|
||||||
ipaddr1: (obj.req == null) ? null : obj.req.clientIp,
|
ipaddr1: ((obj.peer == null) || (obj.peer.req == null)) ? null : obj.peer.req.clientIp,
|
||||||
ipaddr2: ((obj.peer == null) || (obj.peer.req == null)) ? null : obj.peer.req.clientIp,
|
ipaddr2: (obj.req == null) ? null : obj.req.clientIp,
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
protocol: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p),
|
protocol: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p),
|
||||||
nodeid: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.nodeid)
|
nodeid: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.nodeid)
|
||||||
|
|
@ -884,7 +887,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; }
|
if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; }
|
||||||
const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey);
|
const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey);
|
||||||
if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one.
|
if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one.
|
||||||
const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/' + xdomain + 'meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr, soptions: {} };
|
const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr, soptions: {} };
|
||||||
if (user) { command.userid = user._id; }
|
if (user) { command.userid = user._id; }
|
||||||
if (typeof domain.consentmessages == 'object') {
|
if (typeof domain.consentmessages == 'object') {
|
||||||
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
||||||
|
|
@ -893,6 +896,8 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
||||||
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
||||||
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
||||||
|
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
|
||||||
|
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
|
||||||
}
|
}
|
||||||
if (typeof domain.notificationmessages == 'object') {
|
if (typeof domain.notificationmessages == 'object') {
|
||||||
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
||||||
|
|
@ -922,7 +927,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one.
|
if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one.
|
||||||
const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey);
|
const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey);
|
||||||
if (obj.req.query.tcpport != null) {
|
if (obj.req.query.tcpport != null) {
|
||||||
const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: obj.req.query.tcpport, tcpaddr: ((obj.req.query.tcpaddr == null) ? '127.0.0.1' : obj.req.query.tcpaddr), soptions: {} };
|
const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, tcpport: obj.req.query.tcpport, tcpaddr: ((obj.req.query.tcpaddr == null) ? '127.0.0.1' : obj.req.query.tcpaddr), soptions: {} };
|
||||||
if (typeof domain.consentmessages == 'object') {
|
if (typeof domain.consentmessages == 'object') {
|
||||||
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
||||||
if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
|
if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
|
||||||
|
|
@ -930,6 +935,8 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
||||||
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
||||||
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
||||||
|
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
|
||||||
|
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
|
||||||
}
|
}
|
||||||
if (typeof domain.notificationmessages == 'object') {
|
if (typeof domain.notificationmessages == 'object') {
|
||||||
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
||||||
|
|
@ -940,14 +947,15 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command));
|
parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command));
|
||||||
if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); }
|
if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); }
|
||||||
} else if (obj.req.query.udpport != null) {
|
} else if (obj.req.query.udpport != null) {
|
||||||
const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr), soptions: {} };
|
const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr), soptions: {} }; if (typeof domain.consentmessages == 'object') {
|
||||||
if (typeof domain.consentmessages == 'object') {
|
|
||||||
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
|
||||||
if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
|
if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
|
||||||
if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; }
|
if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; }
|
||||||
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
||||||
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
||||||
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
||||||
|
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
|
||||||
|
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
|
||||||
}
|
}
|
||||||
if (typeof domain.notificationmessages == 'object') {
|
if (typeof domain.notificationmessages == 'object') {
|
||||||
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
||||||
|
|
@ -999,6 +1007,8 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
|
||||||
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
|
||||||
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
|
||||||
|
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
|
||||||
|
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
|
||||||
}
|
}
|
||||||
if (typeof domain.notificationmessages == 'object') {
|
if (typeof domain.notificationmessages == 'object') {
|
||||||
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
|
||||||
|
|
@ -1227,6 +1237,7 @@ function CreateLocalRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
else if (req.query.p == 11) { protocolStr = 'SSH-TERM'; }
|
else if (req.query.p == 11) { protocolStr = 'SSH-TERM'; }
|
||||||
else if (req.query.p == 12) { protocolStr = 'VNC'; }
|
else if (req.query.p == 12) { protocolStr = 'VNC'; }
|
||||||
else if (req.query.p == 13) { protocolStr = 'SSH-FILES'; }
|
else if (req.query.p == 13) { protocolStr = 'SSH-FILES'; }
|
||||||
|
else if (req.query.p == 14) { protocolStr = 'Web-TCP'; }
|
||||||
var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 121, msgArgs: [obj.id, protocolStr, obj.host, Math.floor((Date.now() - obj.time) / 1000)], msg: 'Ended local relay session \"' + obj.id + '\", protocol ' + protocolStr + ' to ' + obj.host + ', ' + Math.floor((Date.now() - obj.time) / 1000) + ' second(s)', nodeid: obj.req.query.nodeid, protocol: req.query.p, in: inTraffc, out: outTraffc };
|
var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 121, msgArgs: [obj.id, protocolStr, obj.host, Math.floor((Date.now() - obj.time) / 1000)], msg: 'Ended local relay session \"' + obj.id + '\", protocol ' + protocolStr + ' to ' + obj.host + ', ' + Math.floor((Date.now() - obj.time) / 1000) + ' second(s)', nodeid: obj.req.query.nodeid, protocol: req.query.p, in: inTraffc, out: outTraffc };
|
||||||
if (obj.guestname) { event.guestname = obj.guestname; } // If this is a sharing session, set the guest name here.
|
if (obj.guestname) { event.guestname = obj.guestname; } // If this is a sharing session, set the guest name here.
|
||||||
parent.parent.DispatchEvent(['*', user._id], obj, event);
|
parent.parent.DispatchEvent(['*', user._id], obj, event);
|
||||||
|
|
@ -1281,6 +1292,7 @@ function CreateLocalRelayEx(parent, ws, req, domain, user, cookie) {
|
||||||
else if (req.query.p == 11) { protocolStr = 'SSH-TERM'; }
|
else if (req.query.p == 11) { protocolStr = 'SSH-TERM'; }
|
||||||
else if (req.query.p == 12) { protocolStr = 'VNC'; }
|
else if (req.query.p == 12) { protocolStr = 'VNC'; }
|
||||||
else if (req.query.p == 13) { protocolStr = 'SSH-FILES'; }
|
else if (req.query.p == 13) { protocolStr = 'SSH-FILES'; }
|
||||||
|
else if (req.query.p == 14) { protocolStr = 'Web-TCP'; }
|
||||||
obj.time = Date.now();
|
obj.time = Date.now();
|
||||||
var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 120, msgArgs: [obj.id, protocolStr, obj.host], msg: 'Started local relay session \"' + obj.id + '\", protocol ' + protocolStr + ' to ' + obj.host, nodeid: req.query.nodeid, protocol: req.query.p };
|
var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 120, msgArgs: [obj.id, protocolStr, obj.host], msg: 'Started local relay session \"' + obj.id + '\", protocol ' + protocolStr + ' to ' + obj.host, nodeid: req.query.nodeid, protocol: req.query.p };
|
||||||
if (obj.guestname) { event.guestname = obj.guestname; } // If this is a sharing session, set the guest name here.
|
if (obj.guestname) { event.guestname = obj.guestname; } // If this is a sharing session, set the guest name here.
|
||||||
|
|
|
||||||
943
meshuser.js
943
meshuser.js
File diff suppressed because it is too large
Load diff
117
monitoring.js
Normal file
117
monitoring.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* @description MeshCentral monitoring module
|
||||||
|
* @author Simon Smith
|
||||||
|
* @license Apache-2.0
|
||||||
|
* @version v0.0.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports.CreateMonitoring = function (parent, args) {
|
||||||
|
var obj = {};
|
||||||
|
obj.args = args;
|
||||||
|
obj.parent = parent;
|
||||||
|
obj.express = require('express');
|
||||||
|
obj.app = obj.express();
|
||||||
|
obj.prometheus = null;
|
||||||
|
if (args.compression !== false) { obj.app.use(require('compression')()); }
|
||||||
|
obj.app.disable('x-powered-by');
|
||||||
|
obj.counterMetrics = { // Counter Metrics always start at 0 and increase but never decrease
|
||||||
|
RelayErrors: { description: "Relay Errors" }, // parent.webserver.relaySessionErrorCount
|
||||||
|
UnknownGroup: { description: "Unknown Group" }, // meshDoesNotExistCount
|
||||||
|
InvalidPKCSsignature: { description: "Invalid PKCS signature" }, // invalidPkcsSignatureCount
|
||||||
|
InvalidRSAsignature: { description: "Invalid RSA signature" }, // invalidRsaSignatureCount
|
||||||
|
InvalidJSON: { description: "Invalid JSON" }, // invalidJsonCount
|
||||||
|
UnknownAction: { description: "Unknown Action" }, // unknownAgentActionCount
|
||||||
|
BadWebCertificate: { description: "Bad Web Certificate" }, // agentBadWebCertHashCount
|
||||||
|
BadSignature: { description: "Bad Signature" }, // (agentBadSignature1Count + agentBadSignature2Count)
|
||||||
|
MaxSessionsReached: { description: "Max Sessions Reached" }, // agentMaxSessionHoldCount
|
||||||
|
UnknownDeviceGroup: { description: "Unknown Device Group" }, // (invalidDomainMeshCount + invalidDomainMesh2Count)
|
||||||
|
InvalidDeviceGroupType: { description: "Invalid Device Group Type" }, // invalidMeshTypeCount
|
||||||
|
DuplicateAgent: { description: "Duplicate Agent" }, // duplicateAgentCount
|
||||||
|
blockedUsers: { description: "Blocked Users" }, // blockedUsers
|
||||||
|
blockedAgents: { description: "Blocked Agents" }, // blockedAgents
|
||||||
|
};
|
||||||
|
obj.gaugeMetrics = { // Gauge Metrics always start at 0 and can increase and decrease
|
||||||
|
ConnectedIntelAMT: { description: "Connected Intel AMT" }, // parent.mpsserver.ciraConnections[i].length
|
||||||
|
UserAccounts: { description: "User Accounts" }, // Object.keys(parent.webserver.users).length
|
||||||
|
DeviceGroups: { description: "Device Groups" }, // parent.webserver.meshes (ONLY WHERE deleted=null)
|
||||||
|
AgentSessions: { description: "Agent Sessions" }, // Object.keys(parent.webserver.wsagents).length
|
||||||
|
ConnectedUsers: { description: "Connected Users" }, // Object.keys(parent.webserver.wssessions).length
|
||||||
|
UsersSessions: { description: "Users Sessions" }, // Object.keys(parent.webserver.wssessions2).length
|
||||||
|
RelaySessions: { description: "Relay Sessions" }, // parent.webserver.relaySessionCount
|
||||||
|
RelayCount: { description: "Relay Count" } // Object.keys(parent.webserver.wsrelays).length30bb4fb74dfb758d36be52a7
|
||||||
|
}
|
||||||
|
obj.collectors = [];
|
||||||
|
if (parent.config.settings.prometheus != null) { // Create Prometheus Monitoring Endpoint
|
||||||
|
if ((typeof parent.config.settings.prometheus == 'number') && ((parent.config.settings.prometheus < 1) || (parent.config.settings.prometheus > 65535))) {
|
||||||
|
console.log('Promethus port number is invalid, Prometheus metrics endpoint has be disabled');
|
||||||
|
delete parent.config.settings.prometheus;
|
||||||
|
} else {
|
||||||
|
const port = ((typeof parent.config.settings.prometheus == 'number') ? parent.config.settings.prometheus : 9464);
|
||||||
|
obj.prometheus = require('prom-client');
|
||||||
|
const collectDefaultMetrics = obj.prometheus.collectDefaultMetrics;
|
||||||
|
collectDefaultMetrics();
|
||||||
|
for (const key in obj.gaugeMetrics) {
|
||||||
|
obj.gaugeMetrics[key].prometheus = new obj.prometheus.Gauge({ name: 'meshcentral_' + String(key).toLowerCase(), help: obj.gaugeMetrics[key].description });
|
||||||
|
}
|
||||||
|
for (const key in obj.counterMetrics) {
|
||||||
|
obj.counterMetrics[key].prometheus = new obj.prometheus.Counter({ name: 'meshcentral_' + String(key).toLowerCase(), help: obj.counterMetrics[key].description });
|
||||||
|
}
|
||||||
|
obj.app.get('/', function (req, res) { res.send('MeshCentral Prometheus server.'); });
|
||||||
|
obj.app.listen(port, function () {
|
||||||
|
console.log('MeshCentral Prometheus server running on port ' + port + '.');
|
||||||
|
obj.parent.updateServerState('prometheus-port', port);
|
||||||
|
});
|
||||||
|
obj.app.get('/metrics', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Count the number of device groups that are not deleted
|
||||||
|
var activeDeviceGroups = 0;
|
||||||
|
for (var i in parent.webserver.meshes) { if (parent.webserver.meshes[i].deleted == null) { activeDeviceGroups++; } } // This is not ideal for performance, we want to dome something better.
|
||||||
|
var gauges = {
|
||||||
|
UserAccounts: Object.keys(parent.webserver.users).length,
|
||||||
|
DeviceGroups: activeDeviceGroups,
|
||||||
|
AgentSessions: Object.keys(parent.webserver.wsagents).length,
|
||||||
|
ConnectedUsers: Object.keys(parent.webserver.wssessions).length,
|
||||||
|
UsersSessions: Object.keys(parent.webserver.wssessions2).length,
|
||||||
|
RelaySessions: parent.webserver.relaySessionCount,
|
||||||
|
RelayCount: Object.keys(parent.webserver.wsrelays).length,
|
||||||
|
ConnectedIntelAMT: 0
|
||||||
|
};
|
||||||
|
if (parent.mpsserver != null) {
|
||||||
|
for (var i in parent.mpsserver.ciraConnections) {
|
||||||
|
gauges.ConnectedIntelAMT += parent.mpsserver.ciraConnections[i].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key in gauges) { obj.gaugeMetrics[key].prometheus.set(gauges[key]); }
|
||||||
|
// Take a look at agent errors
|
||||||
|
var agentstats = parent.webserver.getAgentStats();
|
||||||
|
const counters = {
|
||||||
|
RelayErrors: parent.webserver.relaySessionErrorCount,
|
||||||
|
UnknownGroup: agentstats.meshDoesNotExistCount,
|
||||||
|
InvalidPKCSsignature: agentstats.invalidPkcsSignatureCount,
|
||||||
|
InvalidRSAsignature: agentstats.invalidRsaSignatureCount,
|
||||||
|
InvalidJSON: agentstats.invalidJsonCount,
|
||||||
|
UnknownAction: agentstats.unknownAgentActionCount,
|
||||||
|
BadWebCertificate: agentstats.agentBadWebCertHashCount,
|
||||||
|
BadSignature: (agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count),
|
||||||
|
MaxSessionsReached: agentstats.agentMaxSessionHoldCount,
|
||||||
|
UnknownDeviceGroup: (agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count),
|
||||||
|
InvalidDeviceGroupType: (agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count),
|
||||||
|
DuplicateAgent: agentstats.duplicateAgentCount,
|
||||||
|
blockedUsers: parent.webserver.blockedUsers,
|
||||||
|
blockedAgents: parent.webserver.blockedAgents
|
||||||
|
};
|
||||||
|
for (const key in counters) { obj.counterMetrics[key].prometheus.reset(); obj.counterMetrics[key].prometheus.inc(counters[key]); }
|
||||||
|
res.set('Content-Type', obj.prometheus.register.contentType);
|
||||||
|
await Promise.all(obj.collectors.map((collector) => (collector(req, res))));
|
||||||
|
res.end(await obj.prometheus.register.metrics());
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex);
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
@ -586,6 +586,20 @@ module.exports.CreateMultiServer = function (parent, args) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'agentCommand': {
|
||||||
|
if (msg.nodeid != null) {
|
||||||
|
// Route this message to a connected agent
|
||||||
|
var agent = obj.parent.webserver.wsagents[msg.nodeid];
|
||||||
|
if (agent != null) { agent.send(JSON.stringify(msg.command)); }
|
||||||
|
} else if (msg.meshid != null) {
|
||||||
|
// Route this message to all connected agents of this mesh
|
||||||
|
for (var nodeid in obj.parent.webserver.wsagents) {
|
||||||
|
var agent = obj.parent.webserver.wsagents[nodeid];
|
||||||
|
if (agent.dbMeshKey == msg.meshid) { try { agent.send(JSON.stringify(msg.command)); } catch (ex) { } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// Unknown peer server command
|
// Unknown peer server command
|
||||||
console.log('Unknown action from peer server ' + peerServerId + ': ' + msg.action + '.');
|
console.log('Unknown action from peer server ' + peerServerId + ': ' + msg.action + '.');
|
||||||
|
|
|
||||||
1882
package-lock.json
generated
1882
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "meshcentral",
|
"name": "meshcentral",
|
||||||
"version": "1.1.29",
|
"version": "1.1.42",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Remote Device Management",
|
"Remote Device Management",
|
||||||
"Remote Device Monitoring",
|
"Remote Device Monitoring",
|
||||||
|
|
@ -37,21 +37,21 @@
|
||||||
"sample-config-advanced.json"
|
"sample-config-advanced.json"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yetzt/nedb": "1.8.0",
|
"@seald-io/nedb": "4.0.4",
|
||||||
"archiver": "7.0.0",
|
"archiver": "7.0.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"cbor": "5.2.0",
|
"cbor": "5.2.0",
|
||||||
"compression": "1.7.4",
|
"compression": "1.7.5",
|
||||||
"cookie-session": "2.0.0",
|
"cookie-session": "2.1.0",
|
||||||
"express": "4.19.2",
|
"express": "4.21.2",
|
||||||
"express-handlebars": "5.3.5",
|
"express-handlebars": "7.1.3",
|
||||||
"express-ws": "5.0.2",
|
"express-ws": "5.0.2",
|
||||||
"ipcheck": "0.1.0",
|
"ipcheck": "0.1.0",
|
||||||
"minimist": "1.2.8",
|
"minimist": "1.2.8",
|
||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"ua-parser-js": "1.0.37",
|
"ua-parser-js": "1.0.39",
|
||||||
"ws": "8.17.1",
|
"ws": "8.18.0",
|
||||||
"yauzl": "2.10.0"
|
"yauzl": "2.10.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ module.exports.pluginHandler = function (parent) {
|
||||||
try {
|
try {
|
||||||
obj.plugins[p][hookName](...args);
|
obj.plugins[p][hookName](...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error ocurred while running plugin hook" + p + ':' + hookName + ' (' + e + ')');
|
console.log("Error occurred while running plugin hook " + p + ':' + hookName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +205,7 @@ module.exports.pluginHandler = function (parent) {
|
||||||
panel[p].header = obj.plugins[p].on_device_header();
|
panel[p].header = obj.plugins[p].on_device_header();
|
||||||
panel[p].content = obj.plugins[p].on_device_page();
|
panel[p].content = obj.plugins[p].on_device_page();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error ocurred while getting plugin views " + p + ':' + ' (' + e + ')');
|
console.log("Error occurred while getting plugin views " + p + ':' + ' (' + e + ')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
public/images/duo-2fa-250-disable.png
Normal file
BIN
public/images/duo-2fa-250-disable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/duo-2fa-250.png
Normal file
BIN
public/images/duo-2fa-250.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/login/2fa-duo-48.png
Normal file
BIN
public/images/login/2fa-duo-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/login/2fa-duo-96.png
Normal file
BIN
public/images/login/2fa-duo-96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"name": "MeshCentral",
|
|
||||||
"short_name": "MeshCentral",
|
|
||||||
"description": "Open source web based, remote computer management.",
|
|
||||||
"scope": ".",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "fullscreen",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -180,6 +180,7 @@
|
||||||
self.prevClipboardText = null;
|
self.prevClipboardText = null;
|
||||||
self.clipboardReadTimer = setInterval(function(){
|
self.clipboardReadTimer = setInterval(function(){
|
||||||
if(navigator.clipboard.readText != null){
|
if(navigator.clipboard.readText != null){
|
||||||
|
if (Mstsc.browser() == 'firefox') return; // this is needed because firefox pops up a PASTE option every second which is annoying
|
||||||
navigator.clipboard.readText()
|
navigator.clipboard.readText()
|
||||||
.then(function(data){
|
.then(function(data){
|
||||||
if(data != self.prevClipboard){
|
if(data != self.prevClipboard){
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα",
|
||||||
"Connecting...": "Συνδέεται...",
|
"Connecting...": "Συνδέεται...",
|
||||||
"Disconnecting...": "Aποσυνδέεται...",
|
"Disconnecting...": "Aποσυνδέεται...",
|
||||||
"Reconnecting...": "Επανασυνδέεται...",
|
"Reconnecting...": "Επανασυνδέεται...",
|
||||||
|
|
@ -7,19 +8,15 @@
|
||||||
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
|
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
|
||||||
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
|
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
|
||||||
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
|
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
|
||||||
|
"Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή",
|
||||||
"Disconnected": "Αποσυνδέθηκε",
|
"Disconnected": "Αποσυνδέθηκε",
|
||||||
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
|
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
|
||||||
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
|
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
|
||||||
"Password is required": "Απαιτείται ο κωδικός πρόσβασης",
|
"Credentials are required": "Απαιτούνται διαπιστευτήρια",
|
||||||
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
|
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
|
||||||
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
|
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
|
||||||
|
"Drag": "Σύρσιμο",
|
||||||
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
|
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
|
||||||
"viewport drag": "σύρσιμο θεατού πεδίου",
|
|
||||||
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
|
|
||||||
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
|
|
||||||
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
|
|
||||||
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
|
|
||||||
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
|
|
||||||
"Keyboard": "Πληκτρολόγιο",
|
"Keyboard": "Πληκτρολόγιο",
|
||||||
"Show Keyboard": "Εμφάνιση Πληκτρολογίου",
|
"Show Keyboard": "Εμφάνιση Πληκτρολογίου",
|
||||||
"Extra keys": "Επιπλέον πλήκτρα",
|
"Extra keys": "Επιπλέον πλήκτρα",
|
||||||
|
|
@ -28,6 +25,8 @@
|
||||||
"Toggle Ctrl": "Εναλλαγή Ctrl",
|
"Toggle Ctrl": "Εναλλαγή Ctrl",
|
||||||
"Alt": "Alt",
|
"Alt": "Alt",
|
||||||
"Toggle Alt": "Εναλλαγή Alt",
|
"Toggle Alt": "Εναλλαγή Alt",
|
||||||
|
"Toggle Windows": "Εναλλαγή Παράθυρων",
|
||||||
|
"Windows": "Παράθυρα",
|
||||||
"Send Tab": "Αποστολή Tab",
|
"Send Tab": "Αποστολή Tab",
|
||||||
"Tab": "Tab",
|
"Tab": "Tab",
|
||||||
"Esc": "Esc",
|
"Esc": "Esc",
|
||||||
|
|
@ -41,8 +40,7 @@
|
||||||
"Reboot": "Επανεκκίνηση",
|
"Reboot": "Επανεκκίνηση",
|
||||||
"Reset": "Επαναφορά",
|
"Reset": "Επαναφορά",
|
||||||
"Clipboard": "Πρόχειρο",
|
"Clipboard": "Πρόχειρο",
|
||||||
"Clear": "Καθάρισμα",
|
"Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
|
||||||
"Fullscreen": "Πλήρης Οθόνη",
|
|
||||||
"Settings": "Ρυθμίσεις",
|
"Settings": "Ρυθμίσεις",
|
||||||
"Shared Mode": "Κοινόχρηστη Λειτουργία",
|
"Shared Mode": "Κοινόχρηστη Λειτουργία",
|
||||||
"View Only": "Μόνο Θέαση",
|
"View Only": "Μόνο Θέαση",
|
||||||
|
|
@ -52,6 +50,8 @@
|
||||||
"Local Scaling": "Τοπική Κλιμάκωση",
|
"Local Scaling": "Τοπική Κλιμάκωση",
|
||||||
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
|
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
|
||||||
"Advanced": "Για προχωρημένους",
|
"Advanced": "Για προχωρημένους",
|
||||||
|
"Quality:": "Ποιότητα:",
|
||||||
|
"Compression level:": "Επίπεδο συμπίεσης:",
|
||||||
"Repeater ID:": "Repeater ID:",
|
"Repeater ID:": "Repeater ID:",
|
||||||
"WebSocket": "WebSocket",
|
"WebSocket": "WebSocket",
|
||||||
"Encrypt": "Κρυπτογράφηση",
|
"Encrypt": "Κρυπτογράφηση",
|
||||||
|
|
@ -60,10 +60,20 @@
|
||||||
"Path:": "Διαδρομή:",
|
"Path:": "Διαδρομή:",
|
||||||
"Automatic Reconnect": "Αυτόματη επανασύνδεση",
|
"Automatic Reconnect": "Αυτόματη επανασύνδεση",
|
||||||
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
|
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
|
||||||
|
"Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας",
|
||||||
"Logging:": "Καταγραφή:",
|
"Logging:": "Καταγραφή:",
|
||||||
|
"Version:": "Έκδοση:",
|
||||||
"Disconnect": "Αποσύνδεση",
|
"Disconnect": "Αποσύνδεση",
|
||||||
"Connect": "Σύνδεση",
|
"Connect": "Σύνδεση",
|
||||||
|
"Server identity": "Ταυτότητα Διακομιστή",
|
||||||
|
"The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:",
|
||||||
|
"Fingerprint:": "Δακτυλικό αποτύπωμα:",
|
||||||
|
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".",
|
||||||
|
"Approve": "Αποδοχή",
|
||||||
|
"Reject": "Απόρριψη",
|
||||||
|
"Credentials": "Διαπιστευτήρια",
|
||||||
|
"Username:": "Κωδικός Χρήστη:",
|
||||||
"Password:": "Κωδικός Πρόσβασης:",
|
"Password:": "Κωδικός Πρόσβασης:",
|
||||||
"Cancel": "Ακύρωση",
|
"Send Credentials": "Αποστολή Διαπιστευτηρίων",
|
||||||
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas"
|
"Cancel": "Ακύρωση"
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"HTTPS is required for full functionality": "",
|
|
||||||
"Connecting...": "En cours de connexion...",
|
"Connecting...": "En cours de connexion...",
|
||||||
"Disconnecting...": "Déconnexion en cours...",
|
"Disconnecting...": "Déconnexion en cours...",
|
||||||
"Reconnecting...": "Reconnexion en cours...",
|
"Reconnecting...": "Reconnexion en cours...",
|
||||||
|
|
@ -40,7 +39,8 @@
|
||||||
"Reboot": "Redémarrer",
|
"Reboot": "Redémarrer",
|
||||||
"Reset": "Réinitialiser",
|
"Reset": "Réinitialiser",
|
||||||
"Clipboard": "Presse-papiers",
|
"Clipboard": "Presse-papiers",
|
||||||
"Edit clipboard content in the textarea below.": "",
|
"Clear": "Effacer",
|
||||||
|
"Fullscreen": "Plein écran",
|
||||||
"Settings": "Paramètres",
|
"Settings": "Paramètres",
|
||||||
"Shared Mode": "Mode partagé",
|
"Shared Mode": "Mode partagé",
|
||||||
"View Only": "Afficher uniquement",
|
"View Only": "Afficher uniquement",
|
||||||
|
|
@ -65,12 +65,6 @@
|
||||||
"Version:": "Version :",
|
"Version:": "Version :",
|
||||||
"Disconnect": "Déconnecter",
|
"Disconnect": "Déconnecter",
|
||||||
"Connect": "Connecter",
|
"Connect": "Connecter",
|
||||||
"Server identity": "",
|
|
||||||
"The server has provided the following identifying information:": "",
|
|
||||||
"Fingerprint:": "",
|
|
||||||
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "",
|
|
||||||
"Approve": "",
|
|
||||||
"Reject": "",
|
|
||||||
"Username:": "Nom d'utilisateur :",
|
"Username:": "Nom d'utilisateur :",
|
||||||
"Password:": "Mot de passe :",
|
"Password:": "Mot de passe :",
|
||||||
"Send Credentials": "Envoyer les identifiants",
|
"Send Credentials": "Envoyer les identifiants",
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
"Credentials are required": "Le credenziali sono obbligatorie",
|
"Credentials are required": "Le credenziali sono obbligatorie",
|
||||||
"noVNC encountered an error:": "noVNC ha riscontrato un errore:",
|
"noVNC encountered an error:": "noVNC ha riscontrato un errore:",
|
||||||
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
|
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
|
||||||
"Drag": "",
|
|
||||||
"Move/Drag Viewport": "",
|
|
||||||
"Keyboard": "Tastiera",
|
"Keyboard": "Tastiera",
|
||||||
"Show Keyboard": "Mostra tastiera",
|
"Show Keyboard": "Mostra tastiera",
|
||||||
"Extra keys": "Tasti Aggiuntivi",
|
"Extra keys": "Tasti Aggiuntivi",
|
||||||
|
|
@ -44,7 +42,6 @@
|
||||||
"Settings": "Impostazioni",
|
"Settings": "Impostazioni",
|
||||||
"Shared Mode": "Modalità condivisa",
|
"Shared Mode": "Modalità condivisa",
|
||||||
"View Only": "Sola Visualizzazione",
|
"View Only": "Sola Visualizzazione",
|
||||||
"Clip to Window": "",
|
|
||||||
"Scaling Mode:": "Modalità di ridimensionamento:",
|
"Scaling Mode:": "Modalità di ridimensionamento:",
|
||||||
"None": "Nessuna",
|
"None": "Nessuna",
|
||||||
"Local Scaling": "Ridimensionamento Locale",
|
"Local Scaling": "Ridimensionamento Locale",
|
||||||
|
|
@ -61,7 +58,6 @@
|
||||||
"Automatic Reconnect": "Riconnessione Automatica",
|
"Automatic Reconnect": "Riconnessione Automatica",
|
||||||
"Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
|
"Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
|
||||||
"Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
|
"Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
|
||||||
"Logging:": "",
|
|
||||||
"Version:": "Versione:",
|
"Version:": "Versione:",
|
||||||
"Disconnect": "Disconnetti",
|
"Disconnect": "Disconnetti",
|
||||||
"Connect": "Connetti",
|
"Connect": "Connetti",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です",
|
||||||
"Connecting...": "接続しています...",
|
"Connecting...": "接続しています...",
|
||||||
"Disconnecting...": "切断しています...",
|
"Disconnecting...": "切断しています...",
|
||||||
"Reconnecting...": "再接続しています...",
|
"Reconnecting...": "再接続しています...",
|
||||||
|
|
@ -21,10 +22,10 @@
|
||||||
"Extra keys": "追加キー",
|
"Extra keys": "追加キー",
|
||||||
"Show Extra Keys": "追加キーを表示",
|
"Show Extra Keys": "追加キーを表示",
|
||||||
"Ctrl": "Ctrl",
|
"Ctrl": "Ctrl",
|
||||||
"Toggle Ctrl": "Ctrl キーを切り替え",
|
"Toggle Ctrl": "Ctrl キーをトグル",
|
||||||
"Alt": "Alt",
|
"Alt": "Alt",
|
||||||
"Toggle Alt": "Alt キーを切り替え",
|
"Toggle Alt": "Alt キーをトグル",
|
||||||
"Toggle Windows": "Windows キーを切り替え",
|
"Toggle Windows": "Windows キーをトグル",
|
||||||
"Windows": "Windows",
|
"Windows": "Windows",
|
||||||
"Send Tab": "Tab キーを送信",
|
"Send Tab": "Tab キーを送信",
|
||||||
"Tab": "Tab",
|
"Tab": "Tab",
|
||||||
|
|
@ -39,11 +40,11 @@
|
||||||
"Reboot": "再起動",
|
"Reboot": "再起動",
|
||||||
"Reset": "リセット",
|
"Reset": "リセット",
|
||||||
"Clipboard": "クリップボード",
|
"Clipboard": "クリップボード",
|
||||||
"Clear": "クリア",
|
"Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
|
||||||
"Fullscreen": "全画面表示",
|
"Full Screen": "全画面表示",
|
||||||
"Settings": "設定",
|
"Settings": "設定",
|
||||||
"Shared Mode": "共有モード",
|
"Shared Mode": "共有モード",
|
||||||
"View Only": "表示のみ",
|
"View Only": "表示専用",
|
||||||
"Clip to Window": "ウィンドウにクリップ",
|
"Clip to Window": "ウィンドウにクリップ",
|
||||||
"Scaling Mode:": "スケーリングモード:",
|
"Scaling Mode:": "スケーリングモード:",
|
||||||
"None": "なし",
|
"None": "なし",
|
||||||
|
|
@ -60,11 +61,18 @@
|
||||||
"Path:": "パス:",
|
"Path:": "パス:",
|
||||||
"Automatic Reconnect": "自動再接続",
|
"Automatic Reconnect": "自動再接続",
|
||||||
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
|
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
|
||||||
"Show Dot when No Cursor": "カーソルがないときにドットを表示",
|
"Show Dot when No Cursor": "カーソルがないときにドットを表示する",
|
||||||
"Logging:": "ロギング:",
|
"Logging:": "ロギング:",
|
||||||
"Version:": "バージョン:",
|
"Version:": "バージョン:",
|
||||||
"Disconnect": "切断",
|
"Disconnect": "切断",
|
||||||
"Connect": "接続",
|
"Connect": "接続",
|
||||||
|
"Server identity": "サーバーの識別情報",
|
||||||
|
"The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:",
|
||||||
|
"Fingerprint:": "フィンガープリント:",
|
||||||
|
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。",
|
||||||
|
"Approve": "承認",
|
||||||
|
"Reject": "拒否",
|
||||||
|
"Credentials": "資格情報",
|
||||||
"Username:": "ユーザー名:",
|
"Username:": "ユーザー名:",
|
||||||
"Password:": "パスワード:",
|
"Password:": "パスワード:",
|
||||||
"Send Credentials": "資格情報を送信",
|
"Send Credentials": "資格情報を送信",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
|
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
|
||||||
"Connecting...": "Ansluter...",
|
"Connecting...": "Ansluter...",
|
||||||
"Disconnecting...": "Kopplar ner...",
|
"Disconnecting...": "Kopplar ner...",
|
||||||
"Reconnecting...": "Återansluter...",
|
"Reconnecting...": "Återansluter...",
|
||||||
"Internal error": "Internt fel",
|
"Internal error": "Internt fel",
|
||||||
"Must set host": "Du måste specifiera en värd",
|
"Must set host": "Du måste specifiera en värd",
|
||||||
|
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
|
||||||
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
|
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
|
||||||
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
|
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
|
||||||
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
|
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,69 @@
|
||||||
{
|
{
|
||||||
"Connecting...": "连接中...",
|
"Connecting...": "连接中...",
|
||||||
|
"Connected (encrypted) to ": "已连接(已加密)到",
|
||||||
|
"Connected (unencrypted) to ": "已连接(未加密)到",
|
||||||
"Disconnecting...": "正在断开连接...",
|
"Disconnecting...": "正在断开连接...",
|
||||||
"Reconnecting...": "重新连接中...",
|
|
||||||
"Internal error": "内部错误",
|
|
||||||
"Must set host": "请提供主机名",
|
|
||||||
"Connected (encrypted) to ": "已连接到(加密)",
|
|
||||||
"Connected (unencrypted) to ": "已连接到(未加密)",
|
|
||||||
"Something went wrong, connection is closed": "发生错误,连接已关闭",
|
|
||||||
"Failed to connect to server": "无法连接到服务器",
|
|
||||||
"Disconnected": "已断开连接",
|
"Disconnected": "已断开连接",
|
||||||
"New connection has been rejected with reason: ": "连接被拒绝,原因:",
|
"Must set host": "必须设置主机",
|
||||||
"New connection has been rejected": "连接被拒绝",
|
"Reconnecting...": "重新连接中...",
|
||||||
"Password is required": "请提供密码",
|
"Password is required": "请提供密码",
|
||||||
|
"Disconnect timeout": "超时断开",
|
||||||
"noVNC encountered an error:": "noVNC 遇到一个错误:",
|
"noVNC encountered an error:": "noVNC 遇到一个错误:",
|
||||||
"Hide/Show the control bar": "显示/隐藏控制栏",
|
"Hide/Show the control bar": "显示/隐藏控制栏",
|
||||||
"Move/Drag Viewport": "拖放显示范围",
|
"Move/Drag Viewport": "移动/拖动窗口",
|
||||||
"viewport drag": "显示范围拖放",
|
"viewport drag": "窗口拖动",
|
||||||
"Active Mouse Button": "启动鼠标按鍵",
|
"Active Mouse Button": "启动鼠标按键",
|
||||||
"No mousebutton": "禁用鼠标按鍵",
|
"No mousebutton": "禁用鼠标按键",
|
||||||
"Left mousebutton": "鼠标左鍵",
|
"Left mousebutton": "鼠标左键",
|
||||||
"Middle mousebutton": "鼠标中鍵",
|
"Middle mousebutton": "鼠标中键",
|
||||||
"Right mousebutton": "鼠标右鍵",
|
"Right mousebutton": "鼠标右键",
|
||||||
"Keyboard": "键盘",
|
"Keyboard": "键盘",
|
||||||
"Show Keyboard": "显示键盘",
|
"Show Keyboard": "显示键盘",
|
||||||
"Extra keys": "额外按键",
|
"Extra keys": "额外按键",
|
||||||
"Show Extra Keys": "显示额外按键",
|
"Show Extra Keys": "显示额外按键",
|
||||||
"Ctrl": "Ctrl",
|
"Ctrl": "Ctrl",
|
||||||
"Toggle Ctrl": "切换 Ctrl",
|
"Toggle Ctrl": "切换 Ctrl",
|
||||||
|
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
|
||||||
"Alt": "Alt",
|
"Alt": "Alt",
|
||||||
"Toggle Alt": "切换 Alt",
|
"Toggle Alt": "切换 Alt",
|
||||||
"Send Tab": "发送 Tab 键",
|
"Send Tab": "发送 Tab 键",
|
||||||
"Tab": "Tab",
|
"Tab": "Tab",
|
||||||
"Esc": "Esc",
|
"Esc": "Esc",
|
||||||
"Send Escape": "发送 Escape 键",
|
"Send Escape": "发送 Escape 键",
|
||||||
"Ctrl+Alt+Del": "Ctrl-Alt-Del",
|
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
|
||||||
"Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键",
|
"Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键",
|
||||||
"Shutdown/Reboot": "关机/重新启动",
|
"Shutdown/Reboot": "关机/重启",
|
||||||
"Shutdown/Reboot...": "关机/重新启动...",
|
"Shutdown/Reboot...": "关机/重启...",
|
||||||
"Power": "电源",
|
"Power": "电源",
|
||||||
"Shutdown": "关机",
|
"Shutdown": "关机",
|
||||||
"Reboot": "重新启动",
|
"Reboot": "重启",
|
||||||
"Reset": "重置",
|
"Reset": "重置",
|
||||||
"Clipboard": "剪贴板",
|
"Clipboard": "剪贴板",
|
||||||
"Clear": "清除",
|
"Clear": "清除",
|
||||||
"Fullscreen": "全屏",
|
"Fullscreen": "全屏",
|
||||||
"Settings": "设置",
|
"Settings": "设置",
|
||||||
|
"Encrypt": "加密",
|
||||||
"Shared Mode": "分享模式",
|
"Shared Mode": "分享模式",
|
||||||
"View Only": "仅查看",
|
"View Only": "仅查看",
|
||||||
"Clip to Window": "限制/裁切窗口大小",
|
"Clip to Window": "限制/裁切窗口大小",
|
||||||
"Scaling Mode:": "缩放模式:",
|
"Scaling Mode:": "缩放模式:",
|
||||||
"None": "无",
|
"None": "无",
|
||||||
"Local Scaling": "本地缩放",
|
"Local Scaling": "本地缩放",
|
||||||
|
"Local Downscaling": "降低本地尺寸",
|
||||||
"Remote Resizing": "远程调整大小",
|
"Remote Resizing": "远程调整大小",
|
||||||
"Advanced": "高级",
|
"Advanced": "高级",
|
||||||
|
"Local Cursor": "本地光标",
|
||||||
"Repeater ID:": "中继站 ID",
|
"Repeater ID:": "中继站 ID",
|
||||||
"WebSocket": "WebSocket",
|
"WebSocket": "WebSocket",
|
||||||
"Encrypt": "加密",
|
|
||||||
"Host:": "主机:",
|
"Host:": "主机:",
|
||||||
"Port:": "端口:",
|
"Port:": "端口:",
|
||||||
"Path:": "路径:",
|
"Path:": "路径:",
|
||||||
"Automatic Reconnect": "自动重新连接",
|
"Automatic Reconnect": "自动重新连接",
|
||||||
"Reconnect Delay (ms):": "重新连接间隔 (ms):",
|
"Reconnect Delay (ms):": "重新连接间隔 (ms):",
|
||||||
"Logging:": "日志级别:",
|
"Logging:": "日志级别:",
|
||||||
"Disconnect": "中断连接",
|
"Disconnect": "断开连接",
|
||||||
"Connect": "连接",
|
"Connect": "连接",
|
||||||
"Password:": "密码:",
|
"Password:": "密码:",
|
||||||
"Cancel": "取消"
|
"Cancel": "取消",
|
||||||
|
"Canvas not supported.": "不支持 Canvas。"
|
||||||
}
|
}
|
||||||
|
|
@ -16,13 +16,19 @@ export class Localizer {
|
||||||
this.language = 'en';
|
this.language = 'en';
|
||||||
|
|
||||||
// Current dictionary of translations
|
// Current dictionary of translations
|
||||||
this.dictionary = undefined;
|
this._dictionary = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure suitable language based on user preferences
|
// Configure suitable language based on user preferences
|
||||||
setup(supportedLanguages) {
|
async setup(supportedLanguages, baseURL) {
|
||||||
this.language = 'en'; // Default: US English
|
this.language = 'en'; // Default: US English
|
||||||
|
this._dictionary = undefined;
|
||||||
|
|
||||||
|
this._setupLanguage(supportedLanguages);
|
||||||
|
await this._setupDictionary(baseURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupLanguage(supportedLanguages) {
|
||||||
/*
|
/*
|
||||||
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
|
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
|
||||||
* Fall back to navigator.language for other browsers
|
* Fall back to navigator.language for other browsers
|
||||||
|
|
@ -40,12 +46,6 @@ export class Localizer {
|
||||||
.replace("_", "-")
|
.replace("_", "-")
|
||||||
.split("-");
|
.split("-");
|
||||||
|
|
||||||
// Built-in default?
|
|
||||||
if ((userLang[0] === 'en') &&
|
|
||||||
((userLang[1] === undefined) || (userLang[1] === 'us'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First pass: perfect match
|
// First pass: perfect match
|
||||||
for (let j = 0; j < supportedLanguages.length; j++) {
|
for (let j = 0; j < supportedLanguages.length; j++) {
|
||||||
const supLang = supportedLanguages[j]
|
const supLang = supportedLanguages[j]
|
||||||
|
|
@ -64,7 +64,12 @@ export class Localizer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: fallback
|
// Second pass: English fallback
|
||||||
|
if (userLang[0] === 'en') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third pass pass: other fallback
|
||||||
for (let j = 0;j < supportedLanguages.length;j++) {
|
for (let j = 0;j < supportedLanguages.length;j++) {
|
||||||
const supLang = supportedLanguages[j]
|
const supLang = supportedLanguages[j]
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -84,10 +89,32 @@ export class Localizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _setupDictionary(baseURL) {
|
||||||
|
if (baseURL) {
|
||||||
|
if (!baseURL.endsWith("/")) {
|
||||||
|
baseURL = baseURL + "/";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseURL = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.language === "en") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(baseURL + this.language + ".json");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error("" + response.status + " " + response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dictionary = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve localised text
|
// Retrieve localised text
|
||||||
get(id) {
|
get(id) {
|
||||||
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {
|
if (typeof this._dictionary !== 'undefined' &&
|
||||||
return this.dictionary[id];
|
this._dictionary[id]) {
|
||||||
|
return this._dictionary[id];
|
||||||
} else {
|
} else {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -661,7 +661,7 @@ html {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
|
||||||
line-height: 25px;
|
line-height: 1.6;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
|
|
@ -887,7 +887,7 @@ html {
|
||||||
.noVNC_logo {
|
.noVNC_logo {
|
||||||
color:yellow;
|
color:yellow;
|
||||||
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
|
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
|
||||||
line-height:90%;
|
line-height: 0.9;
|
||||||
text-shadow: 0.1em 0.1em 0 black;
|
text-shadow: 0.1em 0.1em 0 black;
|
||||||
}
|
}
|
||||||
.noVNC_logo span{
|
.noVNC_logo span{
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,9 @@ option {
|
||||||
* Checkboxes
|
* Checkboxes
|
||||||
*/
|
*/
|
||||||
input[type=checkbox] {
|
input[type=checkbox] {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
background-image: unset;
|
background-image: unset;
|
||||||
border: 1px solid dimgrey;
|
border: 1px solid dimgrey;
|
||||||
|
|
@ -104,14 +107,11 @@ input[type=checkbox]:checked {
|
||||||
input[type=checkbox]:checked::after {
|
input[type=checkbox]:checked::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block; /* width & height doesn't work on inline elements */
|
display: block; /* width & height doesn't work on inline elements */
|
||||||
position: relative;
|
|
||||||
top: 0;
|
|
||||||
left: 3px;
|
|
||||||
width: 3px;
|
width: 3px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-width: 0 2px 2px 0;
|
border-width: 0 2px 2px 0;
|
||||||
transform: rotate(40deg);
|
transform: rotate(40deg) translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import Keyboard from "../core/input/keyboard.js";
|
||||||
import RFB from "../core/rfb.js";
|
import RFB from "../core/rfb.js";
|
||||||
import * as WebUtil from "./webutil.js";
|
import * as WebUtil from "./webutil.js";
|
||||||
|
|
||||||
|
const PAGE_TITLE = "noVNC";
|
||||||
|
|
||||||
// String validation
|
// String validation
|
||||||
function isAlphaNumeric(str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
|
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)) };
|
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)) };
|
||||||
|
|
@ -89,7 +91,7 @@ const UI = {
|
||||||
// insecure context
|
// insecure context
|
||||||
if (!window.isSecureContext) {
|
if (!window.isSecureContext) {
|
||||||
// FIXME: This gets hidden when connecting
|
// FIXME: This gets hidden when connecting
|
||||||
UI.showStatus(_("HTTPS is required for full functionality"), 'error');
|
UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to fetch version number
|
// Try to fetch version number
|
||||||
|
|
@ -1056,11 +1058,18 @@ const UI = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
|
||||||
|
{ shared: UI.getSetting('shared'),
|
||||||
|
repeaterID: UI.getSetting('repeaterID'),
|
||||||
|
credentials: { password: password } });
|
||||||
|
} catch (exc) {
|
||||||
|
Log.Error("Failed to connect to server: " + exc);
|
||||||
|
UI.updateVisualState('disconnected');
|
||||||
|
UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
|
|
||||||
{ shared: UI.getSetting('shared'),
|
|
||||||
repeaterID: UI.getSetting('repeaterID'),
|
|
||||||
credentials: { password: password } });
|
|
||||||
UI.rfb.addEventListener("connect", UI.connectFinished);
|
UI.rfb.addEventListener("connect", UI.connectFinished);
|
||||||
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
||||||
UI.rfb.addEventListener("serververification", UI.serverVerify);
|
UI.rfb.addEventListener("serververification", UI.serverVerify);
|
||||||
|
|
@ -1166,6 +1175,7 @@ const UI = {
|
||||||
UI.showStatus(_("Disconnected"), 'normal');
|
UI.showStatus(_("Disconnected"), 'normal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.title = PAGE_TITLE;
|
||||||
|
|
||||||
UI.openControlbar();
|
UI.openControlbar();
|
||||||
UI.openConnectPanel();
|
UI.openConnectPanel();
|
||||||
|
|
@ -1739,9 +1749,9 @@ const UI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDesktopName(e) {
|
updateDesktopName(e) {
|
||||||
// UI.desktopName = e.detail.name;
|
UI.desktopName = e.detail.name;
|
||||||
// Display the desktop name in the document title
|
// Display the desktop name in the document title
|
||||||
// document.title = e.detail.name + " - " + PAGE_TITLE;
|
document.title = e.detail.name + " - " + PAGE_TITLE;
|
||||||
},
|
},
|
||||||
|
|
||||||
bell(e) {
|
bell(e) {
|
||||||
|
|
@ -1778,20 +1788,8 @@ const UI = {
|
||||||
|
|
||||||
// Set up translations
|
// Set up translations
|
||||||
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
|
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
|
||||||
l10n.setup(LINGUAS);
|
l10n.setup(LINGUAS, "app/locale/")
|
||||||
if (l10n.language === "en" || l10n.dictionary !== undefined) {
|
.catch(err => Log.Error("Failed to load translations: " + err))
|
||||||
UI.prime();
|
.then(UI.prime);
|
||||||
} else {
|
|
||||||
fetch('app/locale/' + l10n.language + '.json')
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw Error("" + response.status + " " + response.statusText);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((translations) => { l10n.dictionary = translations; })
|
|
||||||
.catch(err => Log.Error("Failed to load translations: " + err))
|
|
||||||
.then(UI.prime);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UI;
|
export default UI;
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,16 @@
|
||||||
* See README.md for usage and integration instructions.
|
* See README.md for usage and integration instructions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { initLogging as mainInitLogging } from '../core/util/logging.js';
|
import * as Log from '../core/util/logging.js';
|
||||||
|
|
||||||
// init log level reading the logging HTTP param
|
// init log level reading the logging HTTP param
|
||||||
export function initLogging(level) {
|
export function initLogging(level) {
|
||||||
"use strict";
|
"use strict";
|
||||||
if (typeof level !== "undefined") {
|
if (typeof level !== "undefined") {
|
||||||
mainInitLogging(level);
|
Log.initLogging(level);
|
||||||
} else {
|
} else {
|
||||||
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
|
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
|
||||||
mainInitLogging(param || undefined);
|
Log.initLogging(param || undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,14 +25,14 @@ export function initLogging(level) {
|
||||||
//
|
//
|
||||||
// For privacy (Using a hastag #, the parameters will not be sent to the server)
|
// For privacy (Using a hastag #, the parameters will not be sent to the server)
|
||||||
// the url can be requested in the following way:
|
// the url can be requested in the following way:
|
||||||
// https://www.example.com#myqueryparam=myvalue&password=secreatvalue
|
// https://www.example.com#myqueryparam=myvalue&password=secretvalue
|
||||||
//
|
//
|
||||||
// Even Mixing public and non public parameters will work:
|
// Even Mixing public and non public parameters will work:
|
||||||
// https://www.example.com?nonsecretparam=example.com#password=secreatvalue
|
// https://www.example.com?nonsecretparam=example.com#password=secretvalue
|
||||||
export function getQueryVar(name, defVal) {
|
export function getQueryVar(name, defVal) {
|
||||||
"use strict";
|
"use strict";
|
||||||
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
|
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
|
||||||
match = ''.concat(document.location.href, window.location.hash).match(re);
|
match = document.location.href.match(re);
|
||||||
if (typeof defVal === 'undefined') { defVal = null; }
|
if (typeof defVal === 'undefined') { defVal = null; }
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|
@ -146,7 +146,7 @@ export function writeSetting(name, value) {
|
||||||
if (window.chrome && window.chrome.storage) {
|
if (window.chrome && window.chrome.storage) {
|
||||||
window.chrome.storage.sync.set(settings);
|
window.chrome.storage.sync.set(settings);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(name, value);
|
localStorageSet(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) {
|
||||||
if ((name in settings) || (window.chrome && window.chrome.storage)) {
|
if ((name in settings) || (window.chrome && window.chrome.storage)) {
|
||||||
value = settings[name];
|
value = settings[name];
|
||||||
} else {
|
} else {
|
||||||
value = localStorage.getItem(name);
|
value = localStorageGet(name);
|
||||||
settings[name] = value;
|
settings[name] = value;
|
||||||
}
|
}
|
||||||
if (typeof value === "undefined") {
|
if (typeof value === "undefined") {
|
||||||
|
|
@ -181,6 +181,70 @@ export function eraseSetting(name) {
|
||||||
if (window.chrome && window.chrome.storage) {
|
if (window.chrome && window.chrome.storage) {
|
||||||
window.chrome.storage.sync.remove(name);
|
window.chrome.storage.sync.remove(name);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(name);
|
localStorageRemove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loggedMsgs = [];
|
||||||
|
function logOnce(msg, level = "warn") {
|
||||||
|
if (!loggedMsgs.includes(msg)) {
|
||||||
|
switch (level) {
|
||||||
|
case "error":
|
||||||
|
Log.Error(msg);
|
||||||
|
break;
|
||||||
|
case "warn":
|
||||||
|
Log.Warn(msg);
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
Log.Debug(msg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.Info(msg);
|
||||||
|
}
|
||||||
|
loggedMsgs.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?";
|
||||||
|
|
||||||
|
function localStorageGet(name) {
|
||||||
|
let r;
|
||||||
|
try {
|
||||||
|
r = localStorage.getItem(name);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException) {
|
||||||
|
logOnce(cookiesMsg);
|
||||||
|
logOnce("'localStorage.getItem(" + name + ")' failed: " + e,
|
||||||
|
"debug");
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
function localStorageSet(name, value) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(name, value);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException) {
|
||||||
|
logOnce(cookiesMsg);
|
||||||
|
logOnce("'localStorage.setItem(" + name + "," + value +
|
||||||
|
")' failed: " + e, "debug");
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function localStorageRemove(name) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(name);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException) {
|
||||||
|
logOnce(cookiesMsg);
|
||||||
|
logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
|
||||||
|
"debug");
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
178
public/novnc/core/crypto/aes.js
Normal file
178
public/novnc/core/crypto/aes.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
export class AESECBCipher {
|
||||||
|
constructor() {
|
||||||
|
this._key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
const cipher = new AESECBCipher;
|
||||||
|
await cipher._importKey(key, extractable, keyUsages);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable, keyUsages) {
|
||||||
|
this._key = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 16 !== 0 || this._key === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 16;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const y = new Uint8Array(await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: new Uint8Array(16),
|
||||||
|
}, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
|
||||||
|
x.set(y, i * 16);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AESEAXCipher {
|
||||||
|
constructor() {
|
||||||
|
this._rawKey = null;
|
||||||
|
this._ctrKey = null;
|
||||||
|
this._cbcKey = null;
|
||||||
|
this._zeroBlock = new Uint8Array(16);
|
||||||
|
this._prefixBlock0 = this._zeroBlock;
|
||||||
|
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||||
|
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-EAX" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptBlock(block) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, block);
|
||||||
|
return new Uint8Array(encrypted).slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initCMAC() {
|
||||||
|
const k1 = await this._encryptBlock(this._zeroBlock);
|
||||||
|
const k2 = new Uint8Array(16);
|
||||||
|
const v = k1[0] >>> 6;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
||||||
|
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
||||||
|
}
|
||||||
|
const lut = [0x0, 0x87, 0x0e, 0x89];
|
||||||
|
k2[14] ^= v >>> 1;
|
||||||
|
k2[15] = (k1[15] << 2) ^ lut[v];
|
||||||
|
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
||||||
|
this._k1 = k1;
|
||||||
|
this._k2 = k2;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptCTR(data, counter) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _decryptCTR(data, counter) {
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _computeCMAC(data, prefixBlock) {
|
||||||
|
if (prefixBlock.length !== 16) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Math.floor(data.length / 16);
|
||||||
|
const m = Math.ceil(data.length / 16);
|
||||||
|
const r = data.length - n * 16;
|
||||||
|
const cbcData = new Uint8Array((m + 1) * 16);
|
||||||
|
cbcData.set(prefixBlock);
|
||||||
|
cbcData.set(data, 16);
|
||||||
|
if (r === 0) {
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[n * 16 + i] ^= this._k1[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cbcData[(n + 1) * 16 + r] = 0x80;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cbcEncrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, cbcData);
|
||||||
|
|
||||||
|
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
||||||
|
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
||||||
|
return mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new AESEAXCipher;
|
||||||
|
await cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key) {
|
||||||
|
this._rawKey = key;
|
||||||
|
this._ctrKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
|
||||||
|
this._cbcKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
|
||||||
|
await this._initCMAC();
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(algorithm, message) {
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const encrypted = await this._encryptCTR(message, nCMAC);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
const res = new Uint8Array(16 + encrypted.length);
|
||||||
|
res.set(encrypted);
|
||||||
|
res.set(mac, encrypted.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(algorithm, data) {
|
||||||
|
const encrypted = data.slice(0, data.length - 16);
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const mac = data.slice(data.length - 16);
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
if (computedMac.length !== mac.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < mac.length; i++) {
|
||||||
|
if (computedMac[i] !== mac[i]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await this._decryptCTR(encrypted, nCMAC);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
public/novnc/core/crypto/bigint.js
Normal file
34
public/novnc/core/crypto/bigint.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export function modPow(b, e, m) {
|
||||||
|
let r = 1n;
|
||||||
|
b = b % m;
|
||||||
|
while (e > 0n) {
|
||||||
|
if ((e & 1n) === 1n) {
|
||||||
|
r = (r * b) % m;
|
||||||
|
}
|
||||||
|
e = e >> 1n;
|
||||||
|
b = (b * b) % m;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bigIntToU8Array(bigint, padLength=0) {
|
||||||
|
let hex = bigint.toString(16);
|
||||||
|
if (padLength === 0) {
|
||||||
|
padLength = Math.ceil(hex.length / 2);
|
||||||
|
}
|
||||||
|
hex = hex.padStart(padLength * 2, '0');
|
||||||
|
const length = hex.length / 2;
|
||||||
|
const arr = new Uint8Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function u8ArrayToBigInt(arr) {
|
||||||
|
let hex = '0x';
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
hex += arr[i].toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
return BigInt(hex);
|
||||||
|
}
|
||||||
90
public/novnc/core/crypto/crypto.js
Normal file
90
public/novnc/core/crypto/crypto.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { AESECBCipher, AESEAXCipher } from "./aes.js";
|
||||||
|
import { DESCBCCipher, DESECBCipher } from "./des.js";
|
||||||
|
import { RSACipher } from "./rsa.js";
|
||||||
|
import { DHCipher } from "./dh.js";
|
||||||
|
import { MD5 } from "./md5.js";
|
||||||
|
|
||||||
|
// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
|
||||||
|
// Both synchronous and asynchronous implmentations are allowed.
|
||||||
|
class LegacyCrypto {
|
||||||
|
constructor() {
|
||||||
|
this._algorithms = {
|
||||||
|
"AES-ECB": AESECBCipher,
|
||||||
|
"AES-EAX": AESEAXCipher,
|
||||||
|
"DES-ECB": DESECBCipher,
|
||||||
|
"DES-CBC": DESCBCCipher,
|
||||||
|
"RSA-PKCS1-v1_5": RSACipher,
|
||||||
|
"DH": DHCipher,
|
||||||
|
"MD5": MD5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.encrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.encrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.decrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.decrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
importKey(format, keyData, algorithm, extractable, keyUsages) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.importKey(keyData, algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKey(algorithm, extractable, keyUsages) {
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.generateKey(algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey(format, key) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
if (typeof key.exportKey !== "function") {
|
||||||
|
throw new Error("key does not support exportKey");
|
||||||
|
}
|
||||||
|
return key.exportKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
digest(algorithm, data) {
|
||||||
|
const alg = this._algorithms[algorithm];
|
||||||
|
if (typeof alg !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, key, length) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.deriveBits !== "function") {
|
||||||
|
throw new Error("key does not support deriveBits");
|
||||||
|
}
|
||||||
|
return key.deriveBits(algorithm, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LegacyCrypto;
|
||||||
330
public/novnc/core/crypto/des.js
Normal file
330
public/novnc/core/crypto/des.js
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
/*
|
||||||
|
* Ported from Flashlight VNC ActionScript implementation:
|
||||||
|
* http://www.wizhelp.com/flashlight-vnc/
|
||||||
|
*
|
||||||
|
* Full attribution follows:
|
||||||
|
*
|
||||||
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* This DES class has been extracted from package Acme.Crypto for use in VNC.
|
||||||
|
* The unnecessary odd parity code has been removed.
|
||||||
|
*
|
||||||
|
* These changes are:
|
||||||
|
* Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* This software is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
*
|
||||||
|
|
||||||
|
* DesCipher - the DES encryption method
|
||||||
|
*
|
||||||
|
* The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:
|
||||||
|
*
|
||||||
|
* Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software
|
||||||
|
* and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
|
||||||
|
* without fee is hereby granted, provided that this copyright notice is kept
|
||||||
|
* intact.
|
||||||
|
*
|
||||||
|
* WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
|
||||||
|
* OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
|
||||||
|
* FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
|
||||||
|
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
|
||||||
|
* CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
|
||||||
|
* PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
|
||||||
|
* NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
|
||||||
|
* SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
|
||||||
|
* SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
|
||||||
|
* PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP
|
||||||
|
* SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
|
||||||
|
* HIGH RISK ACTIVITIES.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The rest is:
|
||||||
|
*
|
||||||
|
* Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
* SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Visit the ACME Labs Java page for up-to-date versions of this and other
|
||||||
|
* fine Java utilities: http://www.acme.com/java/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable comma-spacing */
|
||||||
|
|
||||||
|
// Tables, permutations, S-boxes, etc.
|
||||||
|
const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
|
||||||
|
25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
|
||||||
|
50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
|
||||||
|
totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
|
||||||
|
|
||||||
|
const z = 0x0;
|
||||||
|
let a,b,c,d,e,f;
|
||||||
|
a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
|
||||||
|
const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
|
||||||
|
z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
|
||||||
|
a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
|
||||||
|
c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
|
||||||
|
a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
|
||||||
|
const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
|
||||||
|
a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
|
||||||
|
z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
|
||||||
|
z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
|
||||||
|
a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
|
||||||
|
const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
|
||||||
|
b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
|
||||||
|
c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
|
||||||
|
b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
|
||||||
|
a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
|
||||||
|
const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
|
||||||
|
z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
|
||||||
|
b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
|
||||||
|
c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
|
||||||
|
a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
|
||||||
|
const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
|
||||||
|
a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
|
||||||
|
z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
|
||||||
|
c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
|
||||||
|
a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
|
||||||
|
const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
|
||||||
|
z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
|
||||||
|
b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
|
||||||
|
a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
|
||||||
|
a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
|
||||||
|
const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
|
||||||
|
b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
|
||||||
|
b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
|
||||||
|
z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
|
||||||
|
a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
|
||||||
|
const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
|
||||||
|
c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
|
||||||
|
a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
|
||||||
|
z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
|
||||||
|
|
||||||
|
/* eslint-enable comma-spacing */
|
||||||
|
|
||||||
|
class DES {
|
||||||
|
constructor(password) {
|
||||||
|
this.keys = [];
|
||||||
|
|
||||||
|
// Set the key.
|
||||||
|
const pc1m = [], pcr = [], kn = [];
|
||||||
|
|
||||||
|
for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
|
||||||
|
l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
|
||||||
|
const m = l & 0x7;
|
||||||
|
pc1m[j] = ((password[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 16; ++i) {
|
||||||
|
const m = i << 1;
|
||||||
|
const n = m + 1;
|
||||||
|
kn[m] = kn[n] = 0;
|
||||||
|
for (let o = 28; o < 59; o += 28) {
|
||||||
|
for (let j = o - 28; j < o; ++j) {
|
||||||
|
const l = j + totrot[i];
|
||||||
|
pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let j = 0; j < 24; ++j) {
|
||||||
|
if (pcr[PC2[j]] !== 0) {
|
||||||
|
kn[m] |= 1 << (23 - j);
|
||||||
|
}
|
||||||
|
if (pcr[PC2[j + 24]] !== 0) {
|
||||||
|
kn[n] |= 1 << (23 - j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookey
|
||||||
|
for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
|
||||||
|
const raw0 = kn[rawi++];
|
||||||
|
const raw1 = kn[rawi++];
|
||||||
|
this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
|
||||||
|
this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
|
||||||
|
++KnLi;
|
||||||
|
this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
|
||||||
|
this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x0000003f);
|
||||||
|
++KnLi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt 8 bytes of text
|
||||||
|
enc8(text) {
|
||||||
|
const b = text.slice();
|
||||||
|
let i = 0, l, r, x; // left, right, accumulator
|
||||||
|
|
||||||
|
// Squash 8 bytes to 2 ints
|
||||||
|
l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
||||||
|
r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
||||||
|
|
||||||
|
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 4);
|
||||||
|
x = ((l >>> 16) ^ r) & 0x0000ffff;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 16);
|
||||||
|
x = ((r >>> 2) ^ l) & 0x33333333;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 2);
|
||||||
|
x = ((r >>> 8) ^ l) & 0x00ff00ff;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 8);
|
||||||
|
r = (r << 1) | ((r >>> 31) & 1);
|
||||||
|
x = (l ^ r) & 0xaaaaaaaa;
|
||||||
|
l ^= x;
|
||||||
|
r ^= x;
|
||||||
|
l = (l << 1) | ((l >>> 31) & 1);
|
||||||
|
|
||||||
|
for (let i = 0, keysi = 0; i < 8; ++i) {
|
||||||
|
x = (r << 28) | (r >>> 4);
|
||||||
|
x ^= this.keys[keysi++];
|
||||||
|
let fval = SP7[x & 0x3f];
|
||||||
|
fval |= SP5[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP3[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP1[(x >>> 24) & 0x3f];
|
||||||
|
x = r ^ this.keys[keysi++];
|
||||||
|
fval |= SP8[x & 0x3f];
|
||||||
|
fval |= SP6[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP4[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP2[(x >>> 24) & 0x3f];
|
||||||
|
l ^= fval;
|
||||||
|
x = (l << 28) | (l >>> 4);
|
||||||
|
x ^= this.keys[keysi++];
|
||||||
|
fval = SP7[x & 0x3f];
|
||||||
|
fval |= SP5[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP3[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP1[(x >>> 24) & 0x3f];
|
||||||
|
x = l ^ this.keys[keysi++];
|
||||||
|
fval |= SP8[x & 0x0000003f];
|
||||||
|
fval |= SP6[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP4[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP2[(x >>> 24) & 0x3f];
|
||||||
|
r ^= fval;
|
||||||
|
}
|
||||||
|
|
||||||
|
r = (r << 31) | (r >>> 1);
|
||||||
|
x = (l ^ r) & 0xaaaaaaaa;
|
||||||
|
l ^= x;
|
||||||
|
r ^= x;
|
||||||
|
l = (l << 31) | (l >>> 1);
|
||||||
|
x = ((l >>> 8) ^ r) & 0x00ff00ff;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 8);
|
||||||
|
x = ((l >>> 2) ^ r) & 0x33333333;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 2);
|
||||||
|
x = ((r >>> 16) ^ l) & 0x0000ffff;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 16);
|
||||||
|
x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 4);
|
||||||
|
|
||||||
|
// Spread ints to bytes
|
||||||
|
x = [r, l];
|
||||||
|
for (i = 0; i < 8; i++) {
|
||||||
|
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
|
||||||
|
if (b[i] < 0) { b[i] += 256; } // unsigned
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DESECBCipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESECBCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key, _extractable, _keyUsages) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DESCBCCipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-CBC" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESCBCCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
let y = new Uint8Array(algorithm.iv);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
y[j] ^= plaintext[i * 8 + j];
|
||||||
|
}
|
||||||
|
y = this._cipher.enc8(y);
|
||||||
|
x.set(y, i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
public/novnc/core/crypto/dh.js
Normal file
55
public/novnc/core/crypto/dh.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
class DHPublicKey {
|
||||||
|
constructor(key) {
|
||||||
|
this._key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey() {
|
||||||
|
return this._key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DHCipher {
|
||||||
|
constructor() {
|
||||||
|
this._g = null;
|
||||||
|
this._p = null;
|
||||||
|
this._gBigInt = null;
|
||||||
|
this._pBigInt = null;
|
||||||
|
this._privateKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateKey(algorithm, _extractable) {
|
||||||
|
const cipher = new DHCipher;
|
||||||
|
cipher._generateKey(algorithm);
|
||||||
|
return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateKey(algorithm) {
|
||||||
|
const g = algorithm.g;
|
||||||
|
const p = algorithm.p;
|
||||||
|
this._keyBytes = p.length;
|
||||||
|
this._gBigInt = u8ArrayToBigInt(g);
|
||||||
|
this._pBigInt = u8ArrayToBigInt(p);
|
||||||
|
this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
|
||||||
|
this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
|
||||||
|
this._publicKey = bigIntToU8Array(modPow(
|
||||||
|
this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, length) {
|
||||||
|
const bytes = Math.ceil(length / 8);
|
||||||
|
const pkey = new Uint8Array(algorithm.public);
|
||||||
|
const len = bytes > this._keyBytes ? bytes : this._keyBytes;
|
||||||
|
const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
|
||||||
|
return bigIntToU8Array(secret, len).slice(0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
public/novnc/core/crypto/md5.js
Normal file
82
public/novnc/core/crypto/md5.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2021 The noVNC Authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Performs MD5 hashing on an array of bytes, returns an array of bytes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function MD5(d) {
|
||||||
|
let s = "";
|
||||||
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
s += String.fromCharCode(d[i]);
|
||||||
|
}
|
||||||
|
return M(V(Y(X(s), 8 * s.length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function M(d) {
|
||||||
|
let f = new Uint8Array(d.length);
|
||||||
|
for (let i=0;i<d.length;i++) {
|
||||||
|
f[i] = d.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function X(d) {
|
||||||
|
let r = Array(d.length >> 2);
|
||||||
|
for (let m = 0; m < r.length; m++) r[m] = 0;
|
||||||
|
for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function V(d) {
|
||||||
|
let r = "";
|
||||||
|
for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Y(d, g) {
|
||||||
|
d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;
|
||||||
|
let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;
|
||||||
|
for (let n = 0; n < d.length; n += 16) {
|
||||||
|
let h = m,
|
||||||
|
t = f,
|
||||||
|
g = r,
|
||||||
|
e = i;
|
||||||
|
f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);
|
||||||
|
}
|
||||||
|
return Array(m, f, r, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmn(d, g, m, f, r, i) {
|
||||||
|
return add(rol(add(add(g, d), add(f, i)), r), m);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ff(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g & m | ~g & f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gg(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g & f | m & ~f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hh(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g ^ m ^ f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ii(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(m ^ (g | ~f), d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(d, g) {
|
||||||
|
let m = (65535 & d) + (65535 & g);
|
||||||
|
return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rol(d, g) {
|
||||||
|
return d << g | d >>> 32 - g;
|
||||||
|
}
|
||||||
132
public/novnc/core/crypto/rsa.js
Normal file
132
public/novnc/core/crypto/rsa.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import Base64 from "../base64.js";
|
||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
export class RSACipher {
|
||||||
|
constructor() {
|
||||||
|
this._keyLength = 0;
|
||||||
|
this._keyBytes = 0;
|
||||||
|
this._n = null;
|
||||||
|
this._e = null;
|
||||||
|
this._d = null;
|
||||||
|
this._nBigInt = null;
|
||||||
|
this._eBigInt = null;
|
||||||
|
this._dBigInt = null;
|
||||||
|
this._extractable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "RSA-PKCS1-v1_5" };
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64urlDecode(data) {
|
||||||
|
data = data.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
||||||
|
return Base64.decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_padArray(arr, length) {
|
||||||
|
const res = new Uint8Array(length);
|
||||||
|
res.set(arr, length - arr.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateKey(algorithm, extractable, _keyUsages) {
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._generateKey(algorithm, extractable);
|
||||||
|
return { privateKey: cipher };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _generateKey(algorithm, extractable) {
|
||||||
|
this._keyLength = algorithm.modulusLength;
|
||||||
|
this._keyBytes = Math.ceil(this._keyLength / 8);
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
modulusLength: algorithm.modulusLength,
|
||||||
|
publicExponent: algorithm.publicExponent,
|
||||||
|
hash: {name: "SHA-256"},
|
||||||
|
},
|
||||||
|
true, ["encrypt", "decrypt"]);
|
||||||
|
const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
|
||||||
|
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
||||||
|
this._dBigInt = u8ArrayToBigInt(this._d);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
|
||||||
|
throw new Error("only support importing RSA public key");
|
||||||
|
}
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._importKey(key, extractable);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable) {
|
||||||
|
const n = key.n;
|
||||||
|
const e = key.e;
|
||||||
|
if (n.length !== e.length) {
|
||||||
|
throw new Error("the sizes of modulus and public exponent do not match");
|
||||||
|
}
|
||||||
|
this._keyBytes = n.length;
|
||||||
|
this._keyLength = this._keyBytes * 8;
|
||||||
|
this._n = new Uint8Array(this._keyBytes);
|
||||||
|
this._e = new Uint8Array(this._keyBytes);
|
||||||
|
this._n.set(n);
|
||||||
|
this._e.set(e);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, message) {
|
||||||
|
if (message.length > this._keyBytes - 11) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
||||||
|
window.crypto.getRandomValues(ps);
|
||||||
|
for (let i = 0; i < ps.length; i++) {
|
||||||
|
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
|
||||||
|
}
|
||||||
|
const em = new Uint8Array(this._keyBytes);
|
||||||
|
em[1] = 0x02;
|
||||||
|
em.set(ps, 2);
|
||||||
|
em.set(message, ps.length + 3);
|
||||||
|
const emBigInt = u8ArrayToBigInt(em);
|
||||||
|
const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
|
||||||
|
return bigIntToU8Array(c, this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(_algorithm, message) {
|
||||||
|
if (message.length !== this._keyBytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const msgBigInt = u8ArrayToBigInt(message);
|
||||||
|
const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
||||||
|
const em = bigIntToU8Array(emBigInt, this._keyBytes);
|
||||||
|
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let i = 2;
|
||||||
|
for (; i < em.length; i++) {
|
||||||
|
if (em[i] === 0x00) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i === em.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return em.slice(i + 1, em.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportKey() {
|
||||||
|
if (!this._extractable) {
|
||||||
|
throw new Error("key is not extractable");
|
||||||
|
}
|
||||||
|
return { n: this._n, e: this._e, d: this._d };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,10 +31,7 @@ export default class HextileDecoder {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rQ = sock.rQ;
|
let subencoding = sock.rQpeek8();
|
||||||
let rQi = sock.rQi;
|
|
||||||
|
|
||||||
let subencoding = rQ[rQi]; // Peek
|
|
||||||
if (subencoding > 30) { // Raw
|
if (subencoding > 30) { // Raw
|
||||||
throw new Error("Illegal hextile subencoding (subencoding: " +
|
throw new Error("Illegal hextile subencoding (subencoding: " +
|
||||||
subencoding + ")");
|
subencoding + ")");
|
||||||
|
|
@ -65,7 +62,7 @@ export default class HextileDecoder {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subrects = rQ[rQi + bytes - 1]; // Peek
|
let subrects = sock.rQpeekBytes(bytes).at(-1);
|
||||||
if (subencoding & 0x10) { // SubrectsColoured
|
if (subencoding & 0x10) { // SubrectsColoured
|
||||||
bytes += subrects * (4 + 2);
|
bytes += subrects * (4 + 2);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -79,7 +76,7 @@ export default class HextileDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We know the encoding and have a whole tile
|
// We know the encoding and have a whole tile
|
||||||
rQi++;
|
sock.rQshift8();
|
||||||
if (subencoding === 0) {
|
if (subencoding === 0) {
|
||||||
if (this._lastsubencoding & 0x01) {
|
if (this._lastsubencoding & 0x01) {
|
||||||
// Weird: ignore blanks are RAW
|
// Weird: ignore blanks are RAW
|
||||||
|
|
@ -89,42 +86,36 @@ export default class HextileDecoder {
|
||||||
}
|
}
|
||||||
} else if (subencoding & 0x01) { // Raw
|
} else if (subencoding & 0x01) { // Raw
|
||||||
let pixels = tw * th;
|
let pixels = tw * th;
|
||||||
|
let data = sock.rQshiftBytes(pixels * 4, false);
|
||||||
// Max sure the image is fully opaque
|
// Max sure the image is fully opaque
|
||||||
for (let i = 0;i < pixels;i++) {
|
for (let i = 0;i < pixels;i++) {
|
||||||
rQ[rQi + i * 4 + 3] = 255;
|
data[i * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
display.blitImage(tx, ty, tw, th, rQ, rQi);
|
display.blitImage(tx, ty, tw, th, data, 0);
|
||||||
rQi += bytes - 1;
|
|
||||||
} else {
|
} else {
|
||||||
if (subencoding & 0x02) { // Background
|
if (subencoding & 0x02) { // Background
|
||||||
this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
this._background = new Uint8Array(sock.rQshiftBytes(4));
|
||||||
rQi += 4;
|
|
||||||
}
|
}
|
||||||
if (subencoding & 0x04) { // Foreground
|
if (subencoding & 0x04) { // Foreground
|
||||||
this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
this._foreground = new Uint8Array(sock.rQshiftBytes(4));
|
||||||
rQi += 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._startTile(tx, ty, tw, th, this._background);
|
this._startTile(tx, ty, tw, th, this._background);
|
||||||
if (subencoding & 0x08) { // AnySubrects
|
if (subencoding & 0x08) { // AnySubrects
|
||||||
let subrects = rQ[rQi];
|
let subrects = sock.rQshift8();
|
||||||
rQi++;
|
|
||||||
|
|
||||||
for (let s = 0; s < subrects; s++) {
|
for (let s = 0; s < subrects; s++) {
|
||||||
let color;
|
let color;
|
||||||
if (subencoding & 0x10) { // SubrectsColoured
|
if (subencoding & 0x10) { // SubrectsColoured
|
||||||
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
|
color = sock.rQshiftBytes(4);
|
||||||
rQi += 4;
|
|
||||||
} else {
|
} else {
|
||||||
color = this._foreground;
|
color = this._foreground;
|
||||||
}
|
}
|
||||||
const xy = rQ[rQi];
|
const xy = sock.rQshift8();
|
||||||
rQi++;
|
|
||||||
const sx = (xy >> 4);
|
const sx = (xy >> 4);
|
||||||
const sy = (xy & 0x0f);
|
const sy = (xy & 0x0f);
|
||||||
|
|
||||||
const wh = rQ[rQi];
|
const wh = sock.rQshift8();
|
||||||
rQi++;
|
|
||||||
const sw = (wh >> 4) + 1;
|
const sw = (wh >> 4) + 1;
|
||||||
const sh = (wh & 0x0f) + 1;
|
const sh = (wh & 0x0f) + 1;
|
||||||
|
|
||||||
|
|
@ -133,7 +124,6 @@ export default class HextileDecoder {
|
||||||
}
|
}
|
||||||
this._finishTile(display);
|
this._finishTile(display);
|
||||||
}
|
}
|
||||||
sock.rQi = rQi;
|
|
||||||
this._lastsubencoding = subencoding;
|
this._lastsubencoding = subencoding;
|
||||||
this._tiles--;
|
this._tiles--;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,131 +11,136 @@ export default class JPEGDecoder {
|
||||||
constructor() {
|
constructor() {
|
||||||
// RealVNC will reuse the quantization tables
|
// RealVNC will reuse the quantization tables
|
||||||
// and Huffman tables, so we need to cache them.
|
// and Huffman tables, so we need to cache them.
|
||||||
this._quantTables = [];
|
|
||||||
this._huffmanTables = [];
|
|
||||||
this._cachedQuantTables = [];
|
this._cachedQuantTables = [];
|
||||||
this._cachedHuffmanTables = [];
|
this._cachedHuffmanTables = [];
|
||||||
|
|
||||||
this._jpegLength = 0;
|
|
||||||
this._segments = [];
|
this._segments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
// A rect of JPEG encodings is simply a JPEG file
|
// A rect of JPEG encodings is simply a JPEG file
|
||||||
if (!this._parseJPEG(sock.rQslice(0))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const data = sock.rQshiftBytes(this._jpegLength);
|
|
||||||
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
|
|
||||||
// If there are quantization tables and Huffman tables in the JPEG
|
|
||||||
// image, we can directly render it.
|
|
||||||
display.imageRect(x, y, width, height, "image/jpeg", data);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// Otherwise we need to insert cached tables.
|
|
||||||
const sofIndex = this._segments.findIndex(
|
|
||||||
x => x[1] == 0xC0 || x[1] == 0xC2
|
|
||||||
);
|
|
||||||
if (sofIndex == -1) {
|
|
||||||
throw new Error("Illegal JPEG image without SOF");
|
|
||||||
}
|
|
||||||
let segments = this._segments.slice(0, sofIndex);
|
|
||||||
segments = segments.concat(this._quantTables.length ?
|
|
||||||
this._quantTables :
|
|
||||||
this._cachedQuantTables);
|
|
||||||
segments.push(this._segments[sofIndex]);
|
|
||||||
segments = segments.concat(this._huffmanTables.length ?
|
|
||||||
this._huffmanTables :
|
|
||||||
this._cachedHuffmanTables,
|
|
||||||
this._segments.slice(sofIndex + 1));
|
|
||||||
let length = 0;
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
|
||||||
length += segments[i].length;
|
|
||||||
}
|
|
||||||
const data = new Uint8Array(length);
|
|
||||||
length = 0;
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
|
||||||
data.set(segments[i], length);
|
|
||||||
length += segments[i].length;
|
|
||||||
}
|
|
||||||
display.imageRect(x, y, width, height, "image/jpeg", data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_parseJPEG(buffer) {
|
|
||||||
if (this._quantTables.length != 0) {
|
|
||||||
this._cachedQuantTables = this._quantTables;
|
|
||||||
}
|
|
||||||
if (this._huffmanTables.length != 0) {
|
|
||||||
this._cachedHuffmanTables = this._huffmanTables;
|
|
||||||
}
|
|
||||||
this._quantTables = [];
|
|
||||||
this._huffmanTables = [];
|
|
||||||
this._segments = [];
|
|
||||||
let i = 0;
|
|
||||||
let bufferLength = buffer.length;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
let j = i;
|
let segment = this._readSegment(sock);
|
||||||
if (j + 2 > bufferLength) {
|
if (segment === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (buffer[j] != 0xFF) {
|
|
||||||
throw new Error("Illegal JPEG marker received (byte: " +
|
|
||||||
buffer[j] + ")");
|
|
||||||
}
|
|
||||||
const type = buffer[j+1];
|
|
||||||
j += 2;
|
|
||||||
if (type == 0xD9) {
|
|
||||||
this._jpegLength = j;
|
|
||||||
this._segments.push(buffer.slice(i, j));
|
|
||||||
return true;
|
|
||||||
} else if (type == 0xDA) {
|
|
||||||
// start of scan
|
|
||||||
let hasFoundEndOfScan = false;
|
|
||||||
for (let k = j + 3; k + 1 < bufferLength; k++) {
|
|
||||||
if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
|
|
||||||
!(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
|
|
||||||
j = k;
|
|
||||||
hasFoundEndOfScan = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasFoundEndOfScan) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this._segments.push(buffer.slice(i, j));
|
|
||||||
i = j;
|
|
||||||
continue;
|
|
||||||
} else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
|
|
||||||
// No length after marker
|
|
||||||
this._segments.push(buffer.slice(i, j));
|
|
||||||
i = j;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (j + 2 > bufferLength) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const length = (buffer[j] << 8) + buffer[j+1] - 2;
|
|
||||||
if (length < 0) {
|
|
||||||
throw new Error("Illegal JPEG length received (length: " +
|
|
||||||
length + ")");
|
|
||||||
}
|
|
||||||
j += 2;
|
|
||||||
if (j + length > bufferLength) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
j += length;
|
|
||||||
const segment = buffer.slice(i, j);
|
|
||||||
if (type == 0xC4) {
|
|
||||||
// Huffman tables
|
|
||||||
this._huffmanTables.push(segment);
|
|
||||||
} else if (type == 0xDB) {
|
|
||||||
// Quantization tables
|
|
||||||
this._quantTables.push(segment);
|
|
||||||
}
|
|
||||||
this._segments.push(segment);
|
this._segments.push(segment);
|
||||||
i = j;
|
// End of image?
|
||||||
|
if (segment[1] === 0xD9) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let huffmanTables = [];
|
||||||
|
let quantTables = [];
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
let type = segment[1];
|
||||||
|
if (type === 0xC4) {
|
||||||
|
// Huffman tables
|
||||||
|
huffmanTables.push(segment);
|
||||||
|
} else if (type === 0xDB) {
|
||||||
|
// Quantization tables
|
||||||
|
quantTables.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sofIndex = this._segments.findIndex(
|
||||||
|
x => x[1] == 0xC0 || x[1] == 0xC2
|
||||||
|
);
|
||||||
|
if (sofIndex == -1) {
|
||||||
|
throw new Error("Illegal JPEG image without SOF");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantTables.length === 0) {
|
||||||
|
this._segments.splice(sofIndex+1, 0,
|
||||||
|
...this._cachedQuantTables);
|
||||||
|
}
|
||||||
|
if (huffmanTables.length === 0) {
|
||||||
|
this._segments.splice(sofIndex+1, 0,
|
||||||
|
...this._cachedHuffmanTables);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = 0;
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
length += segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = new Uint8Array(length);
|
||||||
|
length = 0;
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
data.set(segment, length);
|
||||||
|
length += segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.imageRect(x, y, width, height, "image/jpeg", data);
|
||||||
|
|
||||||
|
if (huffmanTables.length !== 0) {
|
||||||
|
this._cachedHuffmanTables = huffmanTables;
|
||||||
|
}
|
||||||
|
if (quantTables.length !== 0) {
|
||||||
|
this._cachedQuantTables = quantTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._segments = [];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readSegment(sock) {
|
||||||
|
if (sock.rQwait("JPEG", 2)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let marker = sock.rQshift8();
|
||||||
|
if (marker != 0xFF) {
|
||||||
|
throw new Error("Illegal JPEG marker received (byte: " +
|
||||||
|
marker + ")");
|
||||||
|
}
|
||||||
|
let type = sock.rQshift8();
|
||||||
|
if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
|
||||||
|
// No length after marker
|
||||||
|
return new Uint8Array([marker, type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("JPEG", 2, 2)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = sock.rQshift16();
|
||||||
|
if (length < 2) {
|
||||||
|
throw new Error("Illegal JPEG length received (length: " +
|
||||||
|
length + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("JPEG", length-2, 4)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra = 0;
|
||||||
|
if (type === 0xDA) {
|
||||||
|
// start of scan
|
||||||
|
extra += 2;
|
||||||
|
while (true) {
|
||||||
|
if (sock.rQwait("JPEG", length-2+extra, 4)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let data = sock.rQpeekBytes(length-2+extra, false);
|
||||||
|
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
|
||||||
|
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
|
||||||
|
extra -= 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
extra++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let segment = new Uint8Array(2 + length + extra);
|
||||||
|
segment[0] = marker;
|
||||||
|
segment[1] = type;
|
||||||
|
segment[2] = length >> 8;
|
||||||
|
segment[3] = length;
|
||||||
|
segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
|
||||||
|
|
||||||
|
return segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,41 +24,34 @@ export default class RawDecoder {
|
||||||
const pixelSize = depth == 8 ? 1 : 4;
|
const pixelSize = depth == 8 ? 1 : 4;
|
||||||
const bytesPerLine = width * pixelSize;
|
const bytesPerLine = width * pixelSize;
|
||||||
|
|
||||||
if (sock.rQwait("RAW", bytesPerLine)) {
|
while (this._lines > 0) {
|
||||||
return false;
|
if (sock.rQwait("RAW", bytesPerLine)) {
|
||||||
}
|
return false;
|
||||||
|
|
||||||
const curY = y + (height - this._lines);
|
|
||||||
const currHeight = Math.min(this._lines,
|
|
||||||
Math.floor(sock.rQlen / bytesPerLine));
|
|
||||||
const pixels = width * currHeight;
|
|
||||||
|
|
||||||
let data = sock.rQ;
|
|
||||||
let index = sock.rQi;
|
|
||||||
|
|
||||||
// Convert data if needed
|
|
||||||
if (depth == 8) {
|
|
||||||
const newdata = new Uint8Array(pixels * 4);
|
|
||||||
for (let i = 0; i < pixels; i++) {
|
|
||||||
newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
|
|
||||||
newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
|
|
||||||
newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
|
|
||||||
newdata[i * 4 + 3] = 255;
|
|
||||||
}
|
}
|
||||||
data = newdata;
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max sure the image is fully opaque
|
const curY = y + (height - this._lines);
|
||||||
for (let i = 0; i < pixels; i++) {
|
|
||||||
data[index + i * 4 + 3] = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
display.blitImage(x, curY, width, currHeight, data, index);
|
let data = sock.rQshiftBytes(bytesPerLine, false);
|
||||||
sock.rQskipBytes(currHeight * bytesPerLine);
|
|
||||||
this._lines -= currHeight;
|
// Convert data if needed
|
||||||
if (this._lines > 0) {
|
if (depth == 8) {
|
||||||
return false;
|
const newdata = new Uint8Array(width * 4);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
data = newdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max sure the image is fully opaque
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
data[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, curY, width, 1, data, 0);
|
||||||
|
this._lines--;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -76,12 +76,8 @@ export default class TightDecoder {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rQi = sock.rQi;
|
let pixel = sock.rQshiftBytes(3);
|
||||||
const rQ = sock.rQ;
|
display.fillRect(x, y, width, height, pixel, false);
|
||||||
|
|
||||||
display.fillRect(x, y, width, height,
|
|
||||||
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
|
|
||||||
sock.rQskipBytes(3);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +285,73 @@ export default class TightDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||||
throw new Error("Gradient filter not implemented");
|
// assume the TPIXEL is 3 bytes long
|
||||||
|
const uncompressedSize = width * height * 3;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (uncompressedSize === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncompressedSize < 12) {
|
||||||
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = sock.rQshiftBytes(uncompressedSize);
|
||||||
|
} else {
|
||||||
|
data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._zlibs[streamId].setInput(data);
|
||||||
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||||
|
this._zlibs[streamId].setInput(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rgbx = new Uint8Array(4 * width * height);
|
||||||
|
|
||||||
|
let rgbxIndex = 0, dataIndex = 0;
|
||||||
|
let left = new Uint8Array(3);
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
const prediction = left[c];
|
||||||
|
const value = data[dataIndex++] + prediction;
|
||||||
|
rgbx[rgbxIndex++] = value;
|
||||||
|
left[c] = value;
|
||||||
|
}
|
||||||
|
rgbx[rgbxIndex++] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperIndex = 0;
|
||||||
|
let upper = new Uint8Array(3),
|
||||||
|
upperleft = new Uint8Array(3);
|
||||||
|
for (let y = 1; y < height; y++) {
|
||||||
|
left.fill(0);
|
||||||
|
upperleft.fill(0);
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
upper[c] = rgbx[upperIndex++];
|
||||||
|
let prediction = left[c] + upper[c] - upperleft[c];
|
||||||
|
if (prediction < 0) {
|
||||||
|
prediction = 0;
|
||||||
|
} else if (prediction > 255) {
|
||||||
|
prediction = 255;
|
||||||
|
}
|
||||||
|
const value = data[dataIndex++] + prediction;
|
||||||
|
rgbx[rgbxIndex++] = value;
|
||||||
|
upperleft[c] = upper[c];
|
||||||
|
left[c] = value;
|
||||||
|
}
|
||||||
|
rgbx[rgbxIndex++] = 255;
|
||||||
|
upperIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, rgbx, 0, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_readData(sock) {
|
_readData(sock) {
|
||||||
|
|
@ -316,7 +378,7 @@ export default class TightDecoder {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = sock.rQshiftBytes(this._len);
|
let data = sock.rQshiftBytes(this._len, false);
|
||||||
this._len = 0;
|
this._len = 0;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default class ZRLEDecoder {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = sock.rQshiftBytes(this._length);
|
const data = sock.rQshiftBytes(this._length, false);
|
||||||
|
|
||||||
this._inflator.setInput(data);
|
this._inflator.setInput(data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
|
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
|
||||||
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
|
import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js";
|
||||||
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
|
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
|
||||||
|
|
||||||
export default class Deflator {
|
export default class Deflator {
|
||||||
|
|
@ -15,9 +15,8 @@ export default class Deflator {
|
||||||
this.strm = new ZStream();
|
this.strm = new ZStream();
|
||||||
this.chunkSize = 1024 * 10 * 10;
|
this.chunkSize = 1024 * 10 * 10;
|
||||||
this.outputBuffer = new Uint8Array(this.chunkSize);
|
this.outputBuffer = new Uint8Array(this.chunkSize);
|
||||||
this.windowBits = 5;
|
|
||||||
|
|
||||||
deflateInit(this.strm, this.windowBits);
|
deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
deflate(inData) {
|
deflate(inData) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default class Display {
|
||||||
this._drawCtx = null;
|
this._drawCtx = null;
|
||||||
|
|
||||||
this._renderQ = []; // queue drawing actions for in-oder rendering
|
this._renderQ = []; // queue drawing actions for in-oder rendering
|
||||||
this._flushing = false;
|
this._flushPromise = null;
|
||||||
|
|
||||||
// the full frame buffer (logical canvas) size
|
// the full frame buffer (logical canvas) size
|
||||||
this._fbWidth = 0;
|
this._fbWidth = 0;
|
||||||
|
|
@ -61,10 +61,6 @@ export default class Display {
|
||||||
|
|
||||||
this._scale = 1.0;
|
this._scale = 1.0;
|
||||||
this._clipViewport = false;
|
this._clipViewport = false;
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
|
||||||
|
|
||||||
this.onflush = () => {}; // A flush request has finished
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
|
@ -306,9 +302,14 @@ export default class Display {
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
if (this._renderQ.length === 0) {
|
if (this._renderQ.length === 0) {
|
||||||
this.onflush();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
this._flushing = true;
|
if (this._flushPromise === null) {
|
||||||
|
this._flushPromise = new Promise((resolve) => {
|
||||||
|
this._flushResolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._flushPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,9 +518,11 @@ export default class Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._renderQ.length === 0 && this._flushing) {
|
if (this._renderQ.length === 0 &&
|
||||||
this._flushing = false;
|
this._flushPromise !== null) {
|
||||||
this.onflush();
|
this._flushResolve();
|
||||||
|
this._flushPromise = null;
|
||||||
|
this._flushResolve = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export const encodings = {
|
||||||
pseudoEncodingLastRect: -224,
|
pseudoEncodingLastRect: -224,
|
||||||
pseudoEncodingCursor: -239,
|
pseudoEncodingCursor: -239,
|
||||||
pseudoEncodingQEMUExtendedKeyEvent: -258,
|
pseudoEncodingQEMUExtendedKeyEvent: -258,
|
||||||
|
pseudoEncodingQEMULedEvent: -261,
|
||||||
pseudoEncodingDesktopName: -307,
|
pseudoEncodingDesktopName: -307,
|
||||||
pseudoEncodingExtendedDesktopSize: -308,
|
pseudoEncodingExtendedDesktopSize: -308,
|
||||||
pseudoEncodingXvp: -309,
|
pseudoEncodingXvp: -309,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ export default class Inflate {
|
||||||
this.strm = new ZStream();
|
this.strm = new ZStream();
|
||||||
this.chunkSize = 1024 * 10 * 10;
|
this.chunkSize = 1024 * 10 * 10;
|
||||||
this.strm.output = new Uint8Array(this.chunkSize);
|
this.strm.output = new Uint8Array(this.chunkSize);
|
||||||
this.windowBits = 5;
|
|
||||||
|
|
||||||
inflateInit(this.strm, this.windowBits);
|
inflateInit(this.strm);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInput(data) {
|
setInput(data) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default class Keyboard {
|
||||||
|
|
||||||
// ===== PRIVATE METHODS =====
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
_sendKeyEvent(keysym, code, down) {
|
_sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
|
||||||
if (down) {
|
if (down) {
|
||||||
this._keyDownList[code] = keysym;
|
this._keyDownList[code] = keysym;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -48,8 +48,9 @@ export default class Keyboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug("onkeyevent " + (down ? "down" : "up") +
|
Log.Debug("onkeyevent " + (down ? "down" : "up") +
|
||||||
", keysym: " + keysym, ", code: " + code);
|
", keysym: " + keysym, ", code: " + code +
|
||||||
this.onkeyevent(keysym, code, down);
|
", numlock: " + numlock + ", capslock: " + capslock);
|
||||||
|
this.onkeyevent(keysym, code, down, numlock, capslock);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getKeyCode(e) {
|
_getKeyCode(e) {
|
||||||
|
|
@ -86,6 +87,14 @@ export default class Keyboard {
|
||||||
_handleKeyDown(e) {
|
_handleKeyDown(e) {
|
||||||
const code = this._getKeyCode(e);
|
const code = this._getKeyCode(e);
|
||||||
let keysym = KeyboardUtil.getKeysym(e);
|
let keysym = KeyboardUtil.getKeysym(e);
|
||||||
|
let numlock = e.getModifierState('NumLock');
|
||||||
|
let capslock = e.getModifierState('CapsLock');
|
||||||
|
|
||||||
|
// getModifierState for NumLock is not supported on mac and ios and always returns false.
|
||||||
|
// Set to null to indicate unknown/unsupported instead.
|
||||||
|
if (browser.isMac() || browser.isIOS()) {
|
||||||
|
numlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Windows doesn't have a proper AltGr, but handles it using
|
// Windows doesn't have a proper AltGr, but handles it using
|
||||||
// fake Ctrl+Alt. However the remote end might not be Windows,
|
// fake Ctrl+Alt. However the remote end might not be Windows,
|
||||||
|
|
@ -107,7 +116,7 @@ export default class Keyboard {
|
||||||
// key to "AltGraph".
|
// key to "AltGraph".
|
||||||
keysym = KeyTable.XK_ISO_Level3_Shift;
|
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||||
} else {
|
} else {
|
||||||
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
|
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +127,8 @@ export default class Keyboard {
|
||||||
// If it's a virtual keyboard then it should be
|
// If it's a virtual keyboard then it should be
|
||||||
// sufficient to just send press and release right
|
// sufficient to just send press and release right
|
||||||
// after each other
|
// after each other
|
||||||
this._sendKeyEvent(keysym, code, true);
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
this._sendKeyEvent(keysym, code, false);
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
|
|
@ -157,8 +166,8 @@ export default class Keyboard {
|
||||||
// while meta is held down
|
// while meta is held down
|
||||||
if ((browser.isMac() || browser.isIOS()) &&
|
if ((browser.isMac() || browser.isIOS()) &&
|
||||||
(e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
|
(e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
|
||||||
this._sendKeyEvent(keysym, code, true);
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
this._sendKeyEvent(keysym, code, false);
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -168,8 +177,8 @@ export default class Keyboard {
|
||||||
// which toggles on each press, but not on release. So pretend
|
// which toggles on each press, but not on release. So pretend
|
||||||
// it was a quick press and release of the button.
|
// it was a quick press and release of the button.
|
||||||
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
||||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
|
||||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -182,8 +191,8 @@ export default class Keyboard {
|
||||||
KeyTable.XK_Hiragana,
|
KeyTable.XK_Hiragana,
|
||||||
KeyTable.XK_Romaji ];
|
KeyTable.XK_Romaji ];
|
||||||
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
|
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
|
||||||
this._sendKeyEvent(keysym, code, true);
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
this._sendKeyEvent(keysym, code, false);
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +208,7 @@ export default class Keyboard {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sendKeyEvent(keysym, code, true);
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleKeyUp(e) {
|
_handleKeyUp(e) {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export function getKeycode(evt) {
|
||||||
// Get 'KeyboardEvent.key', handling legacy browsers
|
// Get 'KeyboardEvent.key', handling legacy browsers
|
||||||
export function getKey(evt) {
|
export function getKey(evt) {
|
||||||
// Are we getting a proper key value?
|
// Are we getting a proper key value?
|
||||||
if (evt.key !== undefined) {
|
if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
|
||||||
// Mozilla isn't fully in sync with the spec yet
|
// Mozilla isn't fully in sync with the spec yet
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case 'OS': return 'Meta';
|
case 'OS': return 'Meta';
|
||||||
|
|
|
||||||
|
|
@ -1,146 +1,25 @@
|
||||||
import Base64 from './base64.js';
|
|
||||||
import { encodeUTF8 } from './util/strings.js';
|
import { encodeUTF8 } from './util/strings.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
|
import legacyCrypto from './crypto/crypto.js';
|
||||||
|
|
||||||
export class AESEAXCipher {
|
class RA2Cipher {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._rawKey = null;
|
this._cipher = null;
|
||||||
this._ctrKey = null;
|
|
||||||
this._cbcKey = null;
|
|
||||||
this._zeroBlock = new Uint8Array(16);
|
|
||||||
this._prefixBlock0 = this._zeroBlock;
|
|
||||||
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
|
||||||
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _encryptBlock(block) {
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt({
|
|
||||||
name: "AES-CBC",
|
|
||||||
iv: this._zeroBlock,
|
|
||||||
}, this._cbcKey, block);
|
|
||||||
return new Uint8Array(encrypted).slice(0, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _initCMAC() {
|
|
||||||
const k1 = await this._encryptBlock(this._zeroBlock);
|
|
||||||
const k2 = new Uint8Array(16);
|
|
||||||
const v = k1[0] >>> 6;
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
|
||||||
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
|
||||||
}
|
|
||||||
const lut = [0x0, 0x87, 0x0e, 0x89];
|
|
||||||
k2[14] ^= v >>> 1;
|
|
||||||
k2[15] = (k1[15] << 2) ^ lut[v];
|
|
||||||
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
|
||||||
this._k1 = k1;
|
|
||||||
this._k2 = k2;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _encryptCTR(data, counter) {
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt({
|
|
||||||
"name": "AES-CTR",
|
|
||||||
counter: counter,
|
|
||||||
length: 128
|
|
||||||
}, this._ctrKey, data);
|
|
||||||
return new Uint8Array(encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _decryptCTR(data, counter) {
|
|
||||||
const decrypted = await window.crypto.subtle.decrypt({
|
|
||||||
"name": "AES-CTR",
|
|
||||||
counter: counter,
|
|
||||||
length: 128
|
|
||||||
}, this._ctrKey, data);
|
|
||||||
return new Uint8Array(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _computeCMAC(data, prefixBlock) {
|
|
||||||
if (prefixBlock.length !== 16) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const n = Math.floor(data.length / 16);
|
|
||||||
const m = Math.ceil(data.length / 16);
|
|
||||||
const r = data.length - n * 16;
|
|
||||||
const cbcData = new Uint8Array((m + 1) * 16);
|
|
||||||
cbcData.set(prefixBlock);
|
|
||||||
cbcData.set(data, 16);
|
|
||||||
if (r === 0) {
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
cbcData[n * 16 + i] ^= this._k1[i];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cbcData[(n + 1) * 16 + r] = 0x80;
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cbcEncrypted = await window.crypto.subtle.encrypt({
|
|
||||||
name: "AES-CBC",
|
|
||||||
iv: this._zeroBlock,
|
|
||||||
}, this._cbcKey, cbcData);
|
|
||||||
|
|
||||||
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
|
||||||
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
|
||||||
return mac;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setKey(key) {
|
|
||||||
this._rawKey = key;
|
|
||||||
this._ctrKey = await window.crypto.subtle.importKey(
|
|
||||||
"raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]);
|
|
||||||
this._cbcKey = await window.crypto.subtle.importKey(
|
|
||||||
"raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]);
|
|
||||||
await this._initCMAC();
|
|
||||||
}
|
|
||||||
|
|
||||||
async encrypt(message, associatedData, nonce) {
|
|
||||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
|
||||||
const encrypted = await this._encryptCTR(message, nCMAC);
|
|
||||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
|
||||||
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
|
||||||
}
|
|
||||||
const res = new Uint8Array(16 + encrypted.length);
|
|
||||||
res.set(encrypted);
|
|
||||||
res.set(mac, encrypted.length);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(encrypted, associatedData, nonce, mac) {
|
|
||||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
|
||||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
|
||||||
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
|
||||||
}
|
|
||||||
if (computedMac.length !== mac.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < mac.length; i++) {
|
|
||||||
if (computedMac[i] !== mac[i]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const res = await this._decryptCTR(encrypted, nCMAC);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RA2Cipher {
|
|
||||||
constructor() {
|
|
||||||
this._cipher = new AESEAXCipher();
|
|
||||||
this._counter = new Uint8Array(16);
|
this._counter = new Uint8Array(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setKey(key) {
|
async setKey(key) {
|
||||||
await this._cipher.setKey(key);
|
this._cipher = await legacyCrypto.importKey(
|
||||||
|
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeMessage(message) {
|
async makeMessage(message) {
|
||||||
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
||||||
const encrypted = await this._cipher.encrypt(message, ad, this._counter);
|
const encrypted = await legacyCrypto.encrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, message);
|
||||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
const res = new Uint8Array(message.length + 2 + 16);
|
const res = new Uint8Array(message.length + 2 + 16);
|
||||||
res.set(ad);
|
res.set(ad);
|
||||||
|
|
@ -148,164 +27,18 @@ export class RA2Cipher {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveMessage(length, encrypted, mac) {
|
async receiveMessage(length, encrypted) {
|
||||||
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
||||||
const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac);
|
const res = await legacyCrypto.decrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, encrypted);
|
||||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RSACipher {
|
|
||||||
constructor(keyLength) {
|
|
||||||
this._key = null;
|
|
||||||
this._keyLength = keyLength;
|
|
||||||
this._keyBytes = Math.ceil(keyLength / 8);
|
|
||||||
this._n = null;
|
|
||||||
this._e = null;
|
|
||||||
this._d = null;
|
|
||||||
this._nBigInt = null;
|
|
||||||
this._eBigInt = null;
|
|
||||||
this._dBigInt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_base64urlDecode(data) {
|
|
||||||
data = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
|
||||||
return Base64.decode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
_u8ArrayToBigInt(arr) {
|
|
||||||
let hex = '0x';
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
hex += arr[i].toString(16).padStart(2, '0');
|
|
||||||
}
|
|
||||||
return BigInt(hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
_padArray(arr, length) {
|
|
||||||
const res = new Uint8Array(length);
|
|
||||||
res.set(arr, length - arr.length);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
_bigIntToU8Array(bigint, padLength=0) {
|
|
||||||
let hex = bigint.toString(16);
|
|
||||||
if (padLength === 0) {
|
|
||||||
padLength = Math.ceil(hex.length / 2) * 2;
|
|
||||||
}
|
|
||||||
hex = hex.padStart(padLength * 2, '0');
|
|
||||||
const length = hex.length / 2;
|
|
||||||
const arr = new Uint8Array(length);
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
_modPow(b, e, m) {
|
|
||||||
if (m === 1n) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let r = 1n;
|
|
||||||
b = b % m;
|
|
||||||
while (e > 0) {
|
|
||||||
if (e % 2n === 1n) {
|
|
||||||
r = (r * b) % m;
|
|
||||||
}
|
|
||||||
e = e / 2n;
|
|
||||||
b = (b * b) % m;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateKey() {
|
|
||||||
this._key = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "RSA-OAEP",
|
|
||||||
modulusLength: this._keyLength,
|
|
||||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
|
||||||
hash: {name: "SHA-256"},
|
|
||||||
},
|
|
||||||
true, ["encrypt", "decrypt"]);
|
|
||||||
const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey);
|
|
||||||
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
|
||||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
|
||||||
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
|
||||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
|
||||||
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
|
||||||
this._dBigInt = this._u8ArrayToBigInt(this._d);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPublicKey(n, e) {
|
|
||||||
if (n.length !== this._keyBytes || e.length !== this._keyBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._n = new Uint8Array(this._keyBytes);
|
|
||||||
this._e = new Uint8Array(this._keyBytes);
|
|
||||||
this._n.set(n);
|
|
||||||
this._e.set(e);
|
|
||||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
|
||||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt(message) {
|
|
||||||
if (message.length > this._keyBytes - 11) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
|
||||||
window.crypto.getRandomValues(ps);
|
|
||||||
for (let i = 0; i < ps.length; i++) {
|
|
||||||
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
|
|
||||||
}
|
|
||||||
const em = new Uint8Array(this._keyBytes);
|
|
||||||
em[1] = 0x02;
|
|
||||||
em.set(ps, 2);
|
|
||||||
em.set(message, ps.length + 3);
|
|
||||||
const emBigInt = this._u8ArrayToBigInt(em);
|
|
||||||
const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt);
|
|
||||||
return this._bigIntToU8Array(c, this._keyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(message) {
|
|
||||||
if (message.length !== this._keyBytes) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const msgBigInt = this._u8ArrayToBigInt(message);
|
|
||||||
const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
|
||||||
const em = this._bigIntToU8Array(emBigInt, this._keyBytes);
|
|
||||||
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let i = 2;
|
|
||||||
for (; i < em.length; i++) {
|
|
||||||
if (em[i] === 0x00) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i === em.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return em.slice(i + 1, em.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyLength() {
|
|
||||||
return this._keyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
get n() {
|
|
||||||
return this._n;
|
|
||||||
}
|
|
||||||
|
|
||||||
get e() {
|
|
||||||
return this._e;
|
|
||||||
}
|
|
||||||
|
|
||||||
get d() {
|
|
||||||
return this._d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
constructor(sock, getCredentials) {
|
constructor(sock, getCredentials) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
this._hasStarted = true;
|
this._hasStarted = true;
|
||||||
// 1: Receive server public key
|
// 1: Receive server public key
|
||||||
await this._waitSockAsync(4);
|
await this._waitSockAsync(4);
|
||||||
const serverKeyLengthBuffer = this._sock.rQslice(0, 4);
|
const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
|
||||||
const serverKeyLength = this._sock.rQshift32();
|
const serverKeyLength = this._sock.rQshift32();
|
||||||
if (serverKeyLength < 1024) {
|
if (serverKeyLength < 1024) {
|
||||||
throw new Error("RA2: server public key is too short: " + serverKeyLength);
|
throw new Error("RA2: server public key is too short: " + serverKeyLength);
|
||||||
|
|
@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
await this._waitSockAsync(serverKeyBytes * 2);
|
await this._waitSockAsync(serverKeyBytes * 2);
|
||||||
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
const serverRSACipher = new RSACipher(serverKeyLength);
|
const serverRSACipher = await legacyCrypto.importKey(
|
||||||
serverRSACipher.setPublicKey(serverN, serverE);
|
"raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
|
||||||
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
||||||
serverPublickey.set(serverKeyLengthBuffer);
|
serverPublickey.set(serverKeyLengthBuffer);
|
||||||
serverPublickey.set(serverN, 4);
|
serverPublickey.set(serverN, 4);
|
||||||
serverPublickey.set(serverE, 4 + serverKeyBytes);
|
serverPublickey.set(serverE, 4 + serverKeyBytes);
|
||||||
|
|
||||||
// verify server public key
|
// verify server public key
|
||||||
|
let approveKey = this._waitApproveKeyAsync();
|
||||||
this.dispatchEvent(new CustomEvent("serververification", {
|
this.dispatchEvent(new CustomEvent("serververification", {
|
||||||
detail: { type: "RSA", publickey: serverPublickey }
|
detail: { type: "RSA", publickey: serverPublickey }
|
||||||
}));
|
}));
|
||||||
await this._waitApproveKeyAsync();
|
await approveKey;
|
||||||
|
|
||||||
// 2: Send client public key
|
// 2: Send client public key
|
||||||
const clientKeyLength = 2048;
|
const clientKeyLength = 2048;
|
||||||
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
||||||
const clientRSACipher = new RSACipher(clientKeyLength);
|
const clientRSACipher = (await legacyCrypto.generateKey({
|
||||||
await clientRSACipher.generateKey();
|
name: "RSA-PKCS1-v1_5",
|
||||||
const clientN = clientRSACipher.n;
|
modulusLength: clientKeyLength,
|
||||||
const clientE = clientRSACipher.e;
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
}, true, ["encrypt"])).privateKey;
|
||||||
|
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
|
||||||
|
const clientN = clientExportedRSAKey.n;
|
||||||
|
const clientE = clientExportedRSAKey.e;
|
||||||
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
||||||
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
||||||
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
||||||
|
|
@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
clientPublicKey[3] = clientKeyLength & 0xff;
|
clientPublicKey[3] = clientKeyLength & 0xff;
|
||||||
clientPublicKey.set(clientN, 4);
|
clientPublicKey.set(clientN, 4);
|
||||||
clientPublicKey.set(clientE, 4 + clientKeyBytes);
|
clientPublicKey.set(clientE, 4 + clientKeyBytes);
|
||||||
this._sock.send(clientPublicKey);
|
this._sock.sQpushBytes(clientPublicKey);
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
// 3: Send client random
|
// 3: Send client random
|
||||||
const clientRandom = new Uint8Array(16);
|
const clientRandom = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(clientRandom);
|
window.crypto.getRandomValues(clientRandom);
|
||||||
const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom);
|
const clientEncryptedRandom = await legacyCrypto.encrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
|
||||||
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
||||||
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
||||||
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
||||||
clientRandomMessage.set(clientEncryptedRandom, 2);
|
clientRandomMessage.set(clientEncryptedRandom, 2);
|
||||||
this._sock.send(clientRandomMessage);
|
this._sock.sQpushBytes(clientRandomMessage);
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
// 4: Receive server random
|
// 4: Receive server random
|
||||||
await this._waitSockAsync(2);
|
await this._waitSockAsync(2);
|
||||||
|
|
@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
throw new Error("RA2: wrong encrypted message length");
|
throw new Error("RA2: wrong encrypted message length");
|
||||||
}
|
}
|
||||||
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
||||||
const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom);
|
const serverRandom = await legacyCrypto.decrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
|
||||||
if (serverRandom === null || serverRandom.length !== 16) {
|
if (serverRandom === null || serverRandom.length !== 16) {
|
||||||
throw new Error("RA2: corrupted server encrypted random");
|
throw new Error("RA2: corrupted server encrypted random");
|
||||||
}
|
}
|
||||||
|
|
@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
|
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
|
||||||
serverHash = new Uint8Array(serverHash);
|
serverHash = new Uint8Array(serverHash);
|
||||||
clientHash = new Uint8Array(clientHash);
|
clientHash = new Uint8Array(clientHash);
|
||||||
this._sock.send(await clientCipher.makeMessage(clientHash));
|
this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
|
||||||
|
this._sock.flush();
|
||||||
await this._waitSockAsync(2 + 20 + 16);
|
await this._waitSockAsync(2 + 20 + 16);
|
||||||
if (this._sock.rQshift16() !== 20) {
|
if (this._sock.rQshift16() !== 20) {
|
||||||
throw new Error("RA2: wrong server hash");
|
throw new Error("RA2: wrong server hash");
|
||||||
}
|
}
|
||||||
const serverHashReceived = await serverCipher.receiveMessage(
|
const serverHashReceived = await serverCipher.receiveMessage(
|
||||||
20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16));
|
20, this._sock.rQshiftBytes(20 + 16));
|
||||||
if (serverHashReceived === null) {
|
if (serverHashReceived === null) {
|
||||||
throw new Error("RA2: failed to authenticate the message");
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
}
|
}
|
||||||
|
|
@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
throw new Error("RA2: wrong subtype");
|
throw new Error("RA2: wrong subtype");
|
||||||
}
|
}
|
||||||
let subtype = (await serverCipher.receiveMessage(
|
let subtype = (await serverCipher.receiveMessage(
|
||||||
1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16)));
|
1, this._sock.rQshiftBytes(1 + 16)));
|
||||||
if (subtype === null) {
|
if (subtype === null) {
|
||||||
throw new Error("RA2: failed to authenticate the message");
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
}
|
}
|
||||||
subtype = subtype[0];
|
subtype = subtype[0];
|
||||||
|
let waitCredentials = this._waitCredentialsAsync(subtype);
|
||||||
if (subtype === 1) {
|
if (subtype === 1) {
|
||||||
if (this._getCredentials().username === undefined ||
|
if (this._getCredentials().username === undefined ||
|
||||||
this._getCredentials().password === undefined) {
|
this._getCredentials().password === undefined) {
|
||||||
|
|
@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
} else {
|
} else {
|
||||||
throw new Error("RA2: wrong subtype");
|
throw new Error("RA2: wrong subtype");
|
||||||
}
|
}
|
||||||
await this._waitCredentialsAsync(subtype);
|
await waitCredentials;
|
||||||
let username;
|
let username;
|
||||||
if (subtype === 1) {
|
if (subtype === 1) {
|
||||||
username = encodeUTF8(this._getCredentials().username).slice(0, 255);
|
username = encodeUTF8(this._getCredentials().username).slice(0, 255);
|
||||||
|
|
@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
for (let i = 0; i < password.length; i++) {
|
for (let i = 0; i < password.length; i++) {
|
||||||
credentials[username.length + 2 + i] = password.charCodeAt(i);
|
credentials[username.length + 2 + i] = password.charCodeAt(i);
|
||||||
}
|
}
|
||||||
this._sock.send(await clientCipher.makeMessage(credentials));
|
this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
|
||||||
|
this._sock.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasStarted() {
|
get hasStarted() {
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js";
|
||||||
import GestureHandler from "./input/gesturehandler.js";
|
import GestureHandler from "./input/gesturehandler.js";
|
||||||
import Cursor from "./util/cursor.js";
|
import Cursor from "./util/cursor.js";
|
||||||
import Websock from "./websock.js";
|
import Websock from "./websock.js";
|
||||||
import DES from "./des.js";
|
|
||||||
import KeyTable from "./input/keysym.js";
|
import KeyTable from "./input/keysym.js";
|
||||||
import XtScancode from "./input/xtscancodes.js";
|
import XtScancode from "./input/xtscancodes.js";
|
||||||
import { encodings } from "./encodings.js";
|
import { encodings } from "./encodings.js";
|
||||||
import RSAAESAuthenticationState from "./ra2.js";
|
import RSAAESAuthenticationState from "./ra2.js";
|
||||||
import { MD5 } from "./util/md5.js";
|
import legacyCrypto from "./crypto/crypto.js";
|
||||||
|
|
||||||
import RawDecoder from "./decoders/raw.js";
|
import RawDecoder from "./decoders/raw.js";
|
||||||
import CopyRectDecoder from "./decoders/copyrect.js";
|
import CopyRectDecoder from "./decoders/copyrect.js";
|
||||||
|
|
@ -258,10 +257,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
Log.Error("Display exception: " + exc);
|
Log.Error("Display exception: " + exc);
|
||||||
throw exc;
|
throw exc;
|
||||||
}
|
}
|
||||||
this._display.onflush = this._onFlush.bind(this);
|
|
||||||
|
|
||||||
this._keyboard = new Keyboard(this._canvas);
|
this._keyboard = new Keyboard(this._canvas);
|
||||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||||
|
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
||||||
|
this._remoteNumLock = null;
|
||||||
|
|
||||||
this._gestures = new GestureHandler();
|
this._gestures = new GestureHandler();
|
||||||
|
|
||||||
|
|
@ -960,7 +960,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMessage() {
|
_handleMessage() {
|
||||||
if (this._sock.rQlen === 0) {
|
if (this._sock.rQwait("message", 1)) {
|
||||||
Log.Warn("handleMessage called on an empty receive queue");
|
Log.Warn("handleMessage called on an empty receive queue");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -977,7 +977,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (!this._normalMsg()) {
|
if (!this._normalMsg()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this._sock.rQlen === 0) {
|
if (this._sock.rQwait("message", 1)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -995,7 +995,35 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleKeyEvent(keysym, code, down) {
|
_handleKeyEvent(keysym, code, down, numlock, capslock) {
|
||||||
|
// If remote state of capslock is known, and it doesn't match the local led state of
|
||||||
|
// the keyboard, we send a capslock keypress first to bring it into sync.
|
||||||
|
// If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
|
||||||
|
// we clear the remote state so that we don't send duplicate or spurious fixes,
|
||||||
|
// since it may take some time to receive the new remote CapsLock state.
|
||||||
|
if (code == 'CapsLock' && down) {
|
||||||
|
this._remoteCapsLock = null;
|
||||||
|
}
|
||||||
|
if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
|
||||||
|
Log.Debug("Fixing remote caps lock");
|
||||||
|
|
||||||
|
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||||
|
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||||
|
// We clear the remote capsLock state when we do this to prevent issues with doing this twice
|
||||||
|
// before we receive an update of the the remote state.
|
||||||
|
this._remoteCapsLock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for numlock is exactly the same.
|
||||||
|
if (code == 'NumLock' && down) {
|
||||||
|
this._remoteNumLock = null;
|
||||||
|
}
|
||||||
|
if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
|
||||||
|
Log.Debug("Fixing remote num lock");
|
||||||
|
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
|
||||||
|
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
|
||||||
|
this._remoteNumLock = null;
|
||||||
|
}
|
||||||
this.sendKey(keysym, code, down);
|
this.sendKey(keysym, code, down);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1383,7 +1411,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
while (repeaterID.length < 250) {
|
while (repeaterID.length < 250) {
|
||||||
repeaterID += "\0";
|
repeaterID += "\0";
|
||||||
}
|
}
|
||||||
this._sock.sendString(repeaterID);
|
this._sock.sQpushString(repeaterID);
|
||||||
|
this._sock.flush();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1393,7 +1422,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
const cversion = "00" + parseInt(this._rfbVersion, 10) +
|
const cversion = "00" + parseInt(this._rfbVersion, 10) +
|
||||||
".00" + ((this._rfbVersion * 10) % 10);
|
".00" + ((this._rfbVersion * 10) % 10);
|
||||||
this._sock.sendString("RFB " + cversion + "\n");
|
this._sock.sQpushString("RFB " + cversion + "\n");
|
||||||
|
this._sock.flush();
|
||||||
Log.Debug('Sent ProtocolVersion: ' + cversion);
|
Log.Debug('Sent ProtocolVersion: ' + cversion);
|
||||||
|
|
||||||
this._rfbInitState = 'Security';
|
this._rfbInitState = 'Security';
|
||||||
|
|
@ -1445,7 +1475,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._fail("Unsupported security types (types: " + types + ")");
|
return this._fail("Unsupported security types (types: " + types + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sock.send([this._rfbAuthScheme]);
|
this._sock.sQpush8(this._rfbAuthScheme);
|
||||||
|
this._sock.flush();
|
||||||
} else {
|
} else {
|
||||||
// Server decides
|
// Server decides
|
||||||
if (this._sock.rQwait("security scheme", 4)) { return false; }
|
if (this._sock.rQwait("security scheme", 4)) { return false; }
|
||||||
|
|
@ -1507,12 +1538,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
|
this._sock.sQpush8(this._rfbCredentials.username.length);
|
||||||
String.fromCharCode(this._rfbCredentials.target.length) +
|
this._sock.sQpush8(this._rfbCredentials.target.length);
|
||||||
this._rfbCredentials.username +
|
this._sock.sQpushString(this._rfbCredentials.username);
|
||||||
this._rfbCredentials.target;
|
this._sock.sQpushString(this._rfbCredentials.target);
|
||||||
this._sock.sendString(xvpAuthStr);
|
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
this._rfbAuthScheme = securityTypeVNCAuth;
|
this._rfbAuthScheme = securityTypeVNCAuth;
|
||||||
|
|
||||||
return this._negotiateAuthentication();
|
return this._negotiateAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1530,7 +1564,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
|
return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sock.send([0, 2]);
|
this._sock.sQpush8(0);
|
||||||
|
this._sock.sQpush8(2);
|
||||||
|
this._sock.flush();
|
||||||
this._rfbVeNCryptState = 1;
|
this._rfbVeNCryptState = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1589,12 +1625,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._fail("Unsupported security types (types: " + subtypes + ")");
|
return this._fail("Unsupported security types (types: " + subtypes + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sock.send([this._rfbAuthScheme >> 24,
|
this._sock.sQpush32(this._rfbAuthScheme);
|
||||||
this._rfbAuthScheme >> 16,
|
this._sock.flush();
|
||||||
this._rfbAuthScheme >> 8,
|
|
||||||
this._rfbAuthScheme]);
|
|
||||||
|
|
||||||
this._rfbVeNCryptState == 4;
|
this._rfbVeNCryptState = 4;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1611,20 +1645,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
const user = encodeUTF8(this._rfbCredentials.username);
|
const user = encodeUTF8(this._rfbCredentials.username);
|
||||||
const pass = encodeUTF8(this._rfbCredentials.password);
|
const pass = encodeUTF8(this._rfbCredentials.password);
|
||||||
|
|
||||||
this._sock.send([
|
this._sock.sQpush32(user.length);
|
||||||
(user.length >> 24) & 0xFF,
|
this._sock.sQpush32(pass.length);
|
||||||
(user.length >> 16) & 0xFF,
|
this._sock.sQpushString(user);
|
||||||
(user.length >> 8) & 0xFF,
|
this._sock.sQpushString(pass);
|
||||||
user.length & 0xFF
|
this._sock.flush();
|
||||||
]);
|
|
||||||
this._sock.send([
|
|
||||||
(pass.length >> 24) & 0xFF,
|
|
||||||
(pass.length >> 16) & 0xFF,
|
|
||||||
(pass.length >> 8) & 0xFF,
|
|
||||||
pass.length & 0xFF
|
|
||||||
]);
|
|
||||||
this._sock.sendString(user);
|
|
||||||
this._sock.sendString(pass);
|
|
||||||
|
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1643,7 +1668,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
// TODO(directxman12): make genDES not require an Array
|
// TODO(directxman12): make genDES not require an Array
|
||||||
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
|
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
|
||||||
const response = RFB.genDES(this._rfbCredentials.password, challenge);
|
const response = RFB.genDES(this._rfbCredentials.password, challenge);
|
||||||
this._sock.send(response);
|
this._sock.sQpushBytes(response);
|
||||||
|
this._sock.flush();
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1661,8 +1687,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (this._rfbCredentials.ardPublicKey != undefined &&
|
if (this._rfbCredentials.ardPublicKey != undefined &&
|
||||||
this._rfbCredentials.ardCredentials != undefined) {
|
this._rfbCredentials.ardCredentials != undefined) {
|
||||||
// if the async web crypto is done return the results
|
// if the async web crypto is done return the results
|
||||||
this._sock.send(this._rfbCredentials.ardCredentials);
|
this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
|
||||||
this._sock.send(this._rfbCredentials.ardPublicKey);
|
this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
|
||||||
|
this._sock.flush();
|
||||||
this._rfbCredentials.ardCredentials = null;
|
this._rfbCredentials.ardCredentials = null;
|
||||||
this._rfbCredentials.ardPublicKey = null;
|
this._rfbCredentials.ardPublicKey = null;
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
|
|
@ -1681,77 +1708,35 @@ export default class RFB extends EventTargetMixin {
|
||||||
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
|
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
|
||||||
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
|
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
|
||||||
|
|
||||||
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength));
|
let clientKey = legacyCrypto.generateKey(
|
||||||
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join('');
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
||||||
|
this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
|
||||||
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_modPow(base, exponent, modulus) {
|
async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
|
||||||
|
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
|
||||||
|
const sharedKey = legacyCrypto.deriveBits(
|
||||||
|
{ name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
|
||||||
|
|
||||||
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
|
||||||
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
||||||
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
||||||
|
|
||||||
let b = BigInt(baseHex);
|
const credentials = window.crypto.getRandomValues(new Uint8Array(128));
|
||||||
let e = BigInt(exponentHex);
|
for (let i = 0; i < username.length; i++) {
|
||||||
let m = BigInt(modulusHex);
|
credentials[i] = username.charCodeAt(i);
|
||||||
let r = 1n;
|
|
||||||
b = b % m;
|
|
||||||
while (e > 0) {
|
|
||||||
if (e % 2n === 1n) {
|
|
||||||
r = (r * b) % m;
|
|
||||||
}
|
|
||||||
e = e / 2n;
|
|
||||||
b = (b * b) % m;
|
|
||||||
}
|
}
|
||||||
let hexResult = r.toString(16);
|
credentials[username.length] = 0;
|
||||||
|
for (let i = 0; i < password.length; i++) {
|
||||||
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) {
|
credentials[64 + i] = password.charCodeAt(i);
|
||||||
hexResult = "0"+hexResult;
|
|
||||||
}
|
}
|
||||||
|
credentials[64 + password.length] = 0;
|
||||||
|
|
||||||
let bytesResult = [];
|
const key = await legacyCrypto.digest("MD5", sharedKey);
|
||||||
for (let c = 0; c < hexResult.length; c += 2) {
|
const cipher = await legacyCrypto.importKey(
|
||||||
bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
|
"raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
|
||||||
}
|
const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
|
||||||
return bytesResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _aesEcbEncrypt(string, key) {
|
|
||||||
// perform AES-ECB blocks
|
|
||||||
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
|
|
||||||
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
|
|
||||||
let data = new Uint8Array(string.length);
|
|
||||||
for (let i = 0; i < string.length; ++i) {
|
|
||||||
data[i] = string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
let encrypted = new Uint8Array(data.length);
|
|
||||||
for (let i=0;i<data.length;i+=16) {
|
|
||||||
let block = data.slice(i, i+16);
|
|
||||||
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
|
|
||||||
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
||||||
);
|
|
||||||
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
|
|
||||||
}
|
|
||||||
return encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
|
|
||||||
// calculate the DH keys
|
|
||||||
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
|
|
||||||
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
|
|
||||||
|
|
||||||
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
|
|
||||||
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
|
||||||
|
|
||||||
let paddedUsername = username + '\0' + padding.substring(0, 63);
|
|
||||||
let paddedPassword = password + '\0' + padding.substring(0, 63);
|
|
||||||
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
|
|
||||||
|
|
||||||
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
|
|
||||||
|
|
||||||
this._rfbCredentials.ardCredentials = encrypted;
|
this._rfbCredentials.ardCredentials = encrypted;
|
||||||
this._rfbCredentials.ardPublicKey = clientPublicKey;
|
this._rfbCredentials.ardPublicKey = clientPublicKey;
|
||||||
|
|
@ -1768,10 +1753,12 @@ export default class RFB extends EventTargetMixin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
|
this._sock.sQpush32(this._rfbCredentials.username.length);
|
||||||
this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
|
this._sock.sQpush32(this._rfbCredentials.password.length);
|
||||||
this._sock.sendString(this._rfbCredentials.username);
|
this._sock.sQpushString(this._rfbCredentials.username);
|
||||||
this._sock.sendString(this._rfbCredentials.password);
|
this._sock.sQpushString(this._rfbCredentials.password);
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1809,7 +1796,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
"vendor or signature");
|
"vendor or signature");
|
||||||
}
|
}
|
||||||
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
|
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
|
||||||
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
|
this._sock.sQpush32(0); // use NOTUNNEL
|
||||||
|
this._sock.flush();
|
||||||
return false; // wait until we receive the sub auth count to continue
|
return false; // wait until we receive the sub auth count to continue
|
||||||
} else {
|
} else {
|
||||||
return this._fail("Server wanted tunnels, but doesn't support " +
|
return this._fail("Server wanted tunnels, but doesn't support " +
|
||||||
|
|
@ -1859,7 +1847,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
for (let authType in clientSupportedTypes) {
|
for (let authType in clientSupportedTypes) {
|
||||||
if (serverSupportedTypes.indexOf(authType) != -1) {
|
if (serverSupportedTypes.indexOf(authType) != -1) {
|
||||||
this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
|
this._sock.sQpush32(clientSupportedTypes[authType]);
|
||||||
|
this._sock.flush();
|
||||||
Log.Debug("Selected authentication type: " + authType);
|
Log.Debug("Selected authentication type: " + authType);
|
||||||
|
|
||||||
switch (authType) {
|
switch (authType) {
|
||||||
|
|
@ -1905,8 +1894,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (e.message !== "disconnect normally") {
|
if (e.message !== "disconnect normally") {
|
||||||
this._fail(e.message);
|
this._fail(e.message);
|
||||||
}
|
}
|
||||||
}).then(() => {
|
})
|
||||||
this.dispatchEvent(new CustomEvent('securityresult'));
|
.then(() => {
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
@ -1934,15 +1923,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
const g = this._sock.rQshiftBytes(8);
|
const g = this._sock.rQshiftBytes(8);
|
||||||
const p = this._sock.rQshiftBytes(8);
|
const p = this._sock.rQshiftBytes(8);
|
||||||
const A = this._sock.rQshiftBytes(8);
|
const A = this._sock.rQshiftBytes(8);
|
||||||
const b = window.crypto.getRandomValues(new Uint8Array(8));
|
const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
|
||||||
const B = new Uint8Array(this._modPow(g, b, p));
|
const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
|
||||||
const secret = new Uint8Array(this._modPow(A, b, p));
|
const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
|
||||||
|
|
||||||
const des = new DES(secret);
|
const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
|
||||||
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
|
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
|
||||||
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
||||||
const usernameBytes = new Uint8Array(256);
|
let usernameBytes = new Uint8Array(256);
|
||||||
const passwordBytes = new Uint8Array(64);
|
let passwordBytes = new Uint8Array(64);
|
||||||
window.crypto.getRandomValues(usernameBytes);
|
window.crypto.getRandomValues(usernameBytes);
|
||||||
window.crypto.getRandomValues(passwordBytes);
|
window.crypto.getRandomValues(passwordBytes);
|
||||||
for (let i = 0; i < username.length; i++) {
|
for (let i = 0; i < username.length; i++) {
|
||||||
|
|
@ -1953,25 +1942,12 @@ export default class RFB extends EventTargetMixin {
|
||||||
passwordBytes[i] = password.charCodeAt(i);
|
passwordBytes[i] = password.charCodeAt(i);
|
||||||
}
|
}
|
||||||
passwordBytes[password.length] = 0;
|
passwordBytes[password.length] = 0;
|
||||||
let x = new Uint8Array(secret);
|
usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
|
||||||
for (let i = 0; i < 32; i++) {
|
passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
|
||||||
for (let j = 0; j < 8; j++) {
|
this._sock.sQpushBytes(B);
|
||||||
x[j] ^= usernameBytes[i * 8 + j];
|
this._sock.sQpushBytes(usernameBytes);
|
||||||
}
|
this._sock.sQpushBytes(passwordBytes);
|
||||||
x = des.enc8(x);
|
this._sock.flush();
|
||||||
usernameBytes.set(x, i * 8);
|
|
||||||
}
|
|
||||||
x = new Uint8Array(secret);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
x[j] ^= passwordBytes[i * 8 + j];
|
|
||||||
}
|
|
||||||
x = des.enc8(x);
|
|
||||||
passwordBytes.set(x, i * 8);
|
|
||||||
}
|
|
||||||
this._sock.send(B);
|
|
||||||
this._sock.send(usernameBytes);
|
|
||||||
this._sock.send(passwordBytes);
|
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1979,7 +1955,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
_negotiateAuthentication() {
|
_negotiateAuthentication() {
|
||||||
switch (this._rfbAuthScheme) {
|
switch (this._rfbAuthScheme) {
|
||||||
case securityTypeNone:
|
case securityTypeNone:
|
||||||
this._rfbInitState = 'SecurityResult';
|
if (this._rfbVersion >= 3.8) {
|
||||||
|
this._rfbInitState = 'SecurityResult';
|
||||||
|
} else {
|
||||||
|
this._rfbInitState = 'ClientInitialisation';
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case securityTypeXVP:
|
case securityTypeXVP:
|
||||||
|
|
@ -2016,13 +1996,6 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleSecurityResult() {
|
_handleSecurityResult() {
|
||||||
// There is no security choice, and hence no security result
|
|
||||||
// until RFB 3.7
|
|
||||||
if (this._rfbVersion < 3.7) {
|
|
||||||
this._rfbInitState = 'ClientInitialisation';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
|
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
|
||||||
|
|
||||||
const status = this._sock.rQshift32();
|
const status = this._sock.rQshift32();
|
||||||
|
|
@ -2158,6 +2131,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
encs.push(encodings.pseudoEncodingDesktopSize);
|
encs.push(encodings.pseudoEncodingDesktopSize);
|
||||||
encs.push(encodings.pseudoEncodingLastRect);
|
encs.push(encodings.pseudoEncodingLastRect);
|
||||||
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
||||||
|
encs.push(encodings.pseudoEncodingQEMULedEvent);
|
||||||
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
|
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
|
||||||
encs.push(encodings.pseudoEncodingXvp);
|
encs.push(encodings.pseudoEncodingXvp);
|
||||||
encs.push(encodings.pseudoEncodingFence);
|
encs.push(encodings.pseudoEncodingFence);
|
||||||
|
|
@ -2199,7 +2173,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._handleSecurityReason();
|
return this._handleSecurityReason();
|
||||||
|
|
||||||
case 'ClientInitialisation':
|
case 'ClientInitialisation':
|
||||||
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
|
this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
|
||||||
|
this._sock.flush();
|
||||||
this._rfbInitState = 'ServerInitialisation';
|
this._rfbInitState = 'ServerInitialisation';
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
@ -2381,7 +2356,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
textData = textData.slice(0, -1);
|
textData = textData.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
textData = textData.replace("\r\n", "\n");
|
textData = textData.replaceAll("\r\n", "\n");
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent(
|
this.dispatchEvent(new CustomEvent(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
|
|
@ -2512,19 +2487,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this._fail("Unexpected server message (type " + msgType + ")");
|
this._fail("Unexpected server message (type " + msgType + ")");
|
||||||
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
|
Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFlush() {
|
|
||||||
this._flushing = false;
|
|
||||||
// Resume processing
|
|
||||||
if (this._sock.rQlen > 0) {
|
|
||||||
this._handleMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_framebufferUpdate() {
|
_framebufferUpdate() {
|
||||||
if (this._FBU.rects === 0) {
|
if (this._FBU.rects === 0) {
|
||||||
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
|
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
|
||||||
|
|
@ -2535,7 +2502,14 @@ export default class RFB extends EventTargetMixin {
|
||||||
// to avoid building up an excessive queue
|
// to avoid building up an excessive queue
|
||||||
if (this._display.pending()) {
|
if (this._display.pending()) {
|
||||||
this._flushing = true;
|
this._flushing = true;
|
||||||
this._display.flush();
|
this._display.flush()
|
||||||
|
.then(() => {
|
||||||
|
this._flushing = false;
|
||||||
|
// Resume processing
|
||||||
|
if (!this._sock.rQwait("message", 1)) {
|
||||||
|
this._handleMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2545,13 +2519,13 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (this._sock.rQwait("rect header", 12)) { return false; }
|
if (this._sock.rQwait("rect header", 12)) { return false; }
|
||||||
/* New FramebufferUpdate */
|
/* New FramebufferUpdate */
|
||||||
|
|
||||||
const hdr = this._sock.rQshiftBytes(12);
|
this._FBU.x = this._sock.rQshift16();
|
||||||
this._FBU.x = (hdr[0] << 8) + hdr[1];
|
this._FBU.y = this._sock.rQshift16();
|
||||||
this._FBU.y = (hdr[2] << 8) + hdr[3];
|
this._FBU.width = this._sock.rQshift16();
|
||||||
this._FBU.width = (hdr[4] << 8) + hdr[5];
|
this._FBU.height = this._sock.rQshift16();
|
||||||
this._FBU.height = (hdr[6] << 8) + hdr[7];
|
this._FBU.encoding = this._sock.rQshift32();
|
||||||
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
|
/* Encodings are signed */
|
||||||
(hdr[10] << 8) + hdr[11], 10);
|
this._FBU.encoding >>= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._handleRect()) {
|
if (!this._handleRect()) {
|
||||||
|
|
@ -2593,6 +2567,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
case encodings.pseudoEncodingExtendedDesktopSize:
|
case encodings.pseudoEncodingExtendedDesktopSize:
|
||||||
return this._handleExtendedDesktopSize();
|
return this._handleExtendedDesktopSize();
|
||||||
|
|
||||||
|
case encodings.pseudoEncodingQEMULedEvent:
|
||||||
|
return this._handleLedEvent();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this._handleDataRect();
|
return this._handleDataRect();
|
||||||
}
|
}
|
||||||
|
|
@ -2770,6 +2747,21 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleLedEvent() {
|
||||||
|
if (this._sock.rQwait("LED Status", 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = this._sock.rQshift8();
|
||||||
|
// ScrollLock state can be retrieved with data & 1. This is currently not needed.
|
||||||
|
let numLock = data & 2 ? true : false;
|
||||||
|
let capsLock = data & 4 ? true : false;
|
||||||
|
this._remoteCapsLock = capsLock;
|
||||||
|
this._remoteNumLock = numLock;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_handleExtendedDesktopSize() {
|
_handleExtendedDesktopSize() {
|
||||||
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
|
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2785,26 +2777,18 @@ export default class RFB extends EventTargetMixin {
|
||||||
const firstUpdate = !this._supportsSetDesktopSize;
|
const firstUpdate = !this._supportsSetDesktopSize;
|
||||||
this._supportsSetDesktopSize = true;
|
this._supportsSetDesktopSize = true;
|
||||||
|
|
||||||
// Normally we only apply the current resize mode after a
|
|
||||||
// window resize event. However there is no such trigger on the
|
|
||||||
// initial connect. And we don't know if the server supports
|
|
||||||
// resizing until we've gotten here.
|
|
||||||
if (firstUpdate) {
|
|
||||||
this._requestRemoteResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._sock.rQskipBytes(1); // number-of-screens
|
this._sock.rQskipBytes(1); // number-of-screens
|
||||||
this._sock.rQskipBytes(3); // padding
|
this._sock.rQskipBytes(3); // padding
|
||||||
|
|
||||||
for (let i = 0; i < numberOfScreens; i += 1) {
|
for (let i = 0; i < numberOfScreens; i += 1) {
|
||||||
// Save the id and flags of the first screen
|
// Save the id and flags of the first screen
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
this._screenID = this._sock.rQshiftBytes(4); // id
|
this._screenID = this._sock.rQshift32(); // id
|
||||||
this._sock.rQskipBytes(2); // x-position
|
this._sock.rQskipBytes(2); // x-position
|
||||||
this._sock.rQskipBytes(2); // y-position
|
this._sock.rQskipBytes(2); // y-position
|
||||||
this._sock.rQskipBytes(2); // width
|
this._sock.rQskipBytes(2); // width
|
||||||
this._sock.rQskipBytes(2); // height
|
this._sock.rQskipBytes(2); // height
|
||||||
this._screenFlags = this._sock.rQshiftBytes(4); // flags
|
this._screenFlags = this._sock.rQshift32(); // flags
|
||||||
} else {
|
} else {
|
||||||
this._sock.rQskipBytes(16);
|
this._sock.rQskipBytes(16);
|
||||||
}
|
}
|
||||||
|
|
@ -2842,6 +2826,14 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._resize(this._FBU.width, this._FBU.height);
|
this._resize(this._FBU.width, this._FBU.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normally we only apply the current resize mode after a
|
||||||
|
// window resize event. However there is no such trigger on the
|
||||||
|
// initial connect. And we don't know if the server supports
|
||||||
|
// resizing until we've gotten here.
|
||||||
|
if (firstUpdate) {
|
||||||
|
this._requestRemoteResize();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2937,28 +2929,22 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
static genDES(password, challenge) {
|
static genDES(password, challenge) {
|
||||||
const passwordChars = password.split('').map(c => c.charCodeAt(0));
|
const passwordChars = password.split('').map(c => c.charCodeAt(0));
|
||||||
return (new DES(passwordChars)).encrypt(challenge);
|
const key = legacyCrypto.importKey(
|
||||||
|
"raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
|
||||||
|
return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Class Methods
|
// Class Methods
|
||||||
RFB.messages = {
|
RFB.messages = {
|
||||||
keyEvent(sock, keysym, down) {
|
keyEvent(sock, keysym, down) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(4); // msg-type
|
||||||
const offset = sock._sQlen;
|
sock.sQpush8(down);
|
||||||
|
|
||||||
buff[offset] = 4; // msg-type
|
sock.sQpush16(0);
|
||||||
buff[offset + 1] = down;
|
|
||||||
|
|
||||||
buff[offset + 2] = 0;
|
sock.sQpush32(keysym);
|
||||||
buff[offset + 3] = 0;
|
|
||||||
|
|
||||||
buff[offset + 4] = (keysym >> 24);
|
|
||||||
buff[offset + 5] = (keysym >> 16);
|
|
||||||
buff[offset + 6] = (keysym >> 8);
|
|
||||||
buff[offset + 7] = keysym;
|
|
||||||
|
|
||||||
sock._sQlen += 8;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -2972,46 +2958,28 @@ RFB.messages = {
|
||||||
return xtScanCode;
|
return xtScanCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(255); // msg-type
|
||||||
const offset = sock._sQlen;
|
sock.sQpush8(0); // sub msg-type
|
||||||
|
|
||||||
buff[offset] = 255; // msg-type
|
sock.sQpush16(down);
|
||||||
buff[offset + 1] = 0; // sub msg-type
|
|
||||||
|
|
||||||
buff[offset + 2] = (down >> 8);
|
sock.sQpush32(keysym);
|
||||||
buff[offset + 3] = down;
|
|
||||||
|
|
||||||
buff[offset + 4] = (keysym >> 24);
|
|
||||||
buff[offset + 5] = (keysym >> 16);
|
|
||||||
buff[offset + 6] = (keysym >> 8);
|
|
||||||
buff[offset + 7] = keysym;
|
|
||||||
|
|
||||||
const RFBkeycode = getRFBkeycode(keycode);
|
const RFBkeycode = getRFBkeycode(keycode);
|
||||||
|
|
||||||
buff[offset + 8] = (RFBkeycode >> 24);
|
sock.sQpush32(RFBkeycode);
|
||||||
buff[offset + 9] = (RFBkeycode >> 16);
|
|
||||||
buff[offset + 10] = (RFBkeycode >> 8);
|
|
||||||
buff[offset + 11] = RFBkeycode;
|
|
||||||
|
|
||||||
sock._sQlen += 12;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
pointerEvent(sock, x, y, mask) {
|
pointerEvent(sock, x, y, mask) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(5); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 5; // msg-type
|
sock.sQpush8(mask);
|
||||||
|
|
||||||
buff[offset + 1] = mask;
|
sock.sQpush16(x);
|
||||||
|
sock.sQpush16(y);
|
||||||
|
|
||||||
buff[offset + 2] = x >> 8;
|
|
||||||
buff[offset + 3] = x;
|
|
||||||
|
|
||||||
buff[offset + 4] = y >> 8;
|
|
||||||
buff[offset + 5] = y;
|
|
||||||
|
|
||||||
sock._sQlen += 6;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -3111,14 +3079,11 @@ RFB.messages = {
|
||||||
},
|
},
|
||||||
|
|
||||||
clientCutText(sock, data, extended = false) {
|
clientCutText(sock, data, extended = false) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(6); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 6; // msg-type
|
sock.sQpush8(0); // padding
|
||||||
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 1] = 0; // padding
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 2] = 0; // padding
|
|
||||||
buff[offset + 3] = 0; // padding
|
|
||||||
|
|
||||||
let length;
|
let length;
|
||||||
if (extended) {
|
if (extended) {
|
||||||
|
|
@ -3127,121 +3092,63 @@ RFB.messages = {
|
||||||
length = data.length;
|
length = data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
buff[offset + 4] = length >> 24;
|
sock.sQpush32(length);
|
||||||
buff[offset + 5] = length >> 16;
|
sock.sQpushBytes(data);
|
||||||
buff[offset + 6] = length >> 8;
|
sock.flush();
|
||||||
buff[offset + 7] = length;
|
|
||||||
|
|
||||||
sock._sQlen += 8;
|
|
||||||
|
|
||||||
// We have to keep track of from where in the data we begin creating the
|
|
||||||
// buffer for the flush in the next iteration.
|
|
||||||
let dataOffset = 0;
|
|
||||||
|
|
||||||
let remaining = data.length;
|
|
||||||
while (remaining > 0) {
|
|
||||||
|
|
||||||
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
|
|
||||||
for (let i = 0; i < flushSize; i++) {
|
|
||||||
buff[sock._sQlen + i] = data[dataOffset + i];
|
|
||||||
}
|
|
||||||
|
|
||||||
sock._sQlen += flushSize;
|
|
||||||
sock.flush();
|
|
||||||
|
|
||||||
remaining -= flushSize;
|
|
||||||
dataOffset += flushSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setDesktopSize(sock, width, height, id, flags) {
|
setDesktopSize(sock, width, height, id, flags) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(251); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 251; // msg-type
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 1] = 0; // padding
|
|
||||||
buff[offset + 2] = width >> 8; // width
|
|
||||||
buff[offset + 3] = width;
|
|
||||||
buff[offset + 4] = height >> 8; // height
|
|
||||||
buff[offset + 5] = height;
|
|
||||||
|
|
||||||
buff[offset + 6] = 1; // number-of-screens
|
sock.sQpush16(width);
|
||||||
buff[offset + 7] = 0; // padding
|
sock.sQpush16(height);
|
||||||
|
|
||||||
|
sock.sQpush8(1); // number-of-screens
|
||||||
|
|
||||||
|
sock.sQpush8(0); // padding
|
||||||
|
|
||||||
// screen array
|
// screen array
|
||||||
buff[offset + 8] = id >> 24; // id
|
sock.sQpush32(id);
|
||||||
buff[offset + 9] = id >> 16;
|
sock.sQpush16(0); // x-position
|
||||||
buff[offset + 10] = id >> 8;
|
sock.sQpush16(0); // y-position
|
||||||
buff[offset + 11] = id;
|
sock.sQpush16(width);
|
||||||
buff[offset + 12] = 0; // x-position
|
sock.sQpush16(height);
|
||||||
buff[offset + 13] = 0;
|
sock.sQpush32(flags);
|
||||||
buff[offset + 14] = 0; // y-position
|
|
||||||
buff[offset + 15] = 0;
|
|
||||||
buff[offset + 16] = width >> 8; // width
|
|
||||||
buff[offset + 17] = width;
|
|
||||||
buff[offset + 18] = height >> 8; // height
|
|
||||||
buff[offset + 19] = height;
|
|
||||||
buff[offset + 20] = flags >> 24; // flags
|
|
||||||
buff[offset + 21] = flags >> 16;
|
|
||||||
buff[offset + 22] = flags >> 8;
|
|
||||||
buff[offset + 23] = flags;
|
|
||||||
|
|
||||||
sock._sQlen += 24;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
clientFence(sock, flags, payload) {
|
clientFence(sock, flags, payload) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(248); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 248; // msg-type
|
sock.sQpush8(0); // padding
|
||||||
|
sock.sQpush8(0); // padding
|
||||||
|
sock.sQpush8(0); // padding
|
||||||
|
|
||||||
buff[offset + 1] = 0; // padding
|
sock.sQpush32(flags);
|
||||||
buff[offset + 2] = 0; // padding
|
|
||||||
buff[offset + 3] = 0; // padding
|
|
||||||
|
|
||||||
buff[offset + 4] = flags >> 24; // flags
|
sock.sQpush8(payload.length);
|
||||||
buff[offset + 5] = flags >> 16;
|
sock.sQpushString(payload);
|
||||||
buff[offset + 6] = flags >> 8;
|
|
||||||
buff[offset + 7] = flags;
|
|
||||||
|
|
||||||
const n = payload.length;
|
|
||||||
|
|
||||||
buff[offset + 8] = n; // length
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
buff[offset + 9 + i] = payload.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
sock._sQlen += 9 + n;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
enableContinuousUpdates(sock, enable, x, y, width, height) {
|
enableContinuousUpdates(sock, enable, x, y, width, height) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(150); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 150; // msg-type
|
sock.sQpush8(enable);
|
||||||
buff[offset + 1] = enable; // enable-flag
|
|
||||||
|
|
||||||
buff[offset + 2] = x >> 8; // x
|
sock.sQpush16(x);
|
||||||
buff[offset + 3] = x;
|
sock.sQpush16(y);
|
||||||
buff[offset + 4] = y >> 8; // y
|
sock.sQpush16(width);
|
||||||
buff[offset + 5] = y;
|
sock.sQpush16(height);
|
||||||
buff[offset + 6] = width >> 8; // width
|
|
||||||
buff[offset + 7] = width;
|
|
||||||
buff[offset + 8] = height >> 8; // height
|
|
||||||
buff[offset + 9] = height;
|
|
||||||
|
|
||||||
sock._sQlen += 10;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
pixelFormat(sock, depth, trueColor) {
|
pixelFormat(sock, depth, trueColor) {
|
||||||
const buff = sock._sQ;
|
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
let bpp;
|
let bpp;
|
||||||
|
|
||||||
if (depth > 16) {
|
if (depth > 16) {
|
||||||
|
|
@ -3254,100 +3161,69 @@ RFB.messages = {
|
||||||
|
|
||||||
const bits = Math.floor(depth/3);
|
const bits = Math.floor(depth/3);
|
||||||
|
|
||||||
buff[offset] = 0; // msg-type
|
sock.sQpush8(0); // msg-type
|
||||||
|
|
||||||
buff[offset + 1] = 0; // padding
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 2] = 0; // padding
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 3] = 0; // padding
|
sock.sQpush8(0); // padding
|
||||||
|
|
||||||
buff[offset + 4] = bpp; // bits-per-pixel
|
sock.sQpush8(bpp);
|
||||||
buff[offset + 5] = depth; // depth
|
sock.sQpush8(depth);
|
||||||
buff[offset + 6] = 0; // little-endian
|
sock.sQpush8(0); // little-endian
|
||||||
buff[offset + 7] = trueColor ? 1 : 0; // true-color
|
sock.sQpush8(trueColor ? 1 : 0);
|
||||||
|
|
||||||
buff[offset + 8] = 0; // red-max
|
sock.sQpush16((1 << bits) - 1); // red-max
|
||||||
buff[offset + 9] = (1 << bits) - 1; // red-max
|
sock.sQpush16((1 << bits) - 1); // green-max
|
||||||
|
sock.sQpush16((1 << bits) - 1); // blue-max
|
||||||
|
|
||||||
buff[offset + 10] = 0; // green-max
|
sock.sQpush8(bits * 0); // red-shift
|
||||||
buff[offset + 11] = (1 << bits) - 1; // green-max
|
sock.sQpush8(bits * 1); // green-shift
|
||||||
|
sock.sQpush8(bits * 2); // blue-shift
|
||||||
|
|
||||||
buff[offset + 12] = 0; // blue-max
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 13] = (1 << bits) - 1; // blue-max
|
sock.sQpush8(0); // padding
|
||||||
|
sock.sQpush8(0); // padding
|
||||||
|
|
||||||
buff[offset + 14] = bits * 0; // red-shift
|
|
||||||
buff[offset + 15] = bits * 1; // green-shift
|
|
||||||
buff[offset + 16] = bits * 2; // blue-shift
|
|
||||||
|
|
||||||
buff[offset + 17] = 0; // padding
|
|
||||||
buff[offset + 18] = 0; // padding
|
|
||||||
buff[offset + 19] = 0; // padding
|
|
||||||
|
|
||||||
sock._sQlen += 20;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
clientEncodings(sock, encodings) {
|
clientEncodings(sock, encodings) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(2); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 2; // msg-type
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 1] = 0; // padding
|
|
||||||
|
|
||||||
buff[offset + 2] = encodings.length >> 8;
|
sock.sQpush16(encodings.length);
|
||||||
buff[offset + 3] = encodings.length;
|
|
||||||
|
|
||||||
let j = offset + 4;
|
|
||||||
for (let i = 0; i < encodings.length; i++) {
|
for (let i = 0; i < encodings.length; i++) {
|
||||||
const enc = encodings[i];
|
sock.sQpush32(encodings[i]);
|
||||||
buff[j] = enc >> 24;
|
|
||||||
buff[j + 1] = enc >> 16;
|
|
||||||
buff[j + 2] = enc >> 8;
|
|
||||||
buff[j + 3] = enc;
|
|
||||||
|
|
||||||
j += 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sock._sQlen += j - offset;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
fbUpdateRequest(sock, incremental, x, y, w, h) {
|
fbUpdateRequest(sock, incremental, x, y, w, h) {
|
||||||
const buff = sock._sQ;
|
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
if (typeof(x) === "undefined") { x = 0; }
|
if (typeof(x) === "undefined") { x = 0; }
|
||||||
if (typeof(y) === "undefined") { y = 0; }
|
if (typeof(y) === "undefined") { y = 0; }
|
||||||
|
|
||||||
buff[offset] = 3; // msg-type
|
sock.sQpush8(3); // msg-type
|
||||||
buff[offset + 1] = incremental ? 1 : 0;
|
|
||||||
|
|
||||||
buff[offset + 2] = (x >> 8) & 0xFF;
|
sock.sQpush8(incremental ? 1 : 0);
|
||||||
buff[offset + 3] = x & 0xFF;
|
|
||||||
|
|
||||||
buff[offset + 4] = (y >> 8) & 0xFF;
|
sock.sQpush16(x);
|
||||||
buff[offset + 5] = y & 0xFF;
|
sock.sQpush16(y);
|
||||||
|
sock.sQpush16(w);
|
||||||
|
sock.sQpush16(h);
|
||||||
|
|
||||||
buff[offset + 6] = (w >> 8) & 0xFF;
|
|
||||||
buff[offset + 7] = w & 0xFF;
|
|
||||||
|
|
||||||
buff[offset + 8] = (h >> 8) & 0xFF;
|
|
||||||
buff[offset + 9] = h & 0xFF;
|
|
||||||
|
|
||||||
sock._sQlen += 10;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
xvpOp(sock, ver, op) {
|
xvpOp(sock, ver, op) {
|
||||||
const buff = sock._sQ;
|
sock.sQpush8(250); // msg-type
|
||||||
const offset = sock._sQlen;
|
|
||||||
|
|
||||||
buff[offset] = 250; // msg-type
|
sock.sQpush8(0); // padding
|
||||||
buff[offset + 1] = 0; // padding
|
|
||||||
|
|
||||||
buff[offset + 2] = ver;
|
sock.sQpush8(ver);
|
||||||
buff[offset + 3] = op;
|
sock.sQpush8(op);
|
||||||
|
|
||||||
sock._sQlen += 4;
|
|
||||||
sock.flush();
|
sock.flush();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@ export default class Cursor {
|
||||||
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||||
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||||
|
|
||||||
document.body.removeChild(this._canvas);
|
if (document.contains(this._canvas)) {
|
||||||
|
document.body.removeChild(this._canvas);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._target = null;
|
this._target = null;
|
||||||
|
|
|
||||||
|
|
@ -94,27 +94,7 @@ export default class Websock {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
get sQ() {
|
|
||||||
return this._sQ;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rQ() {
|
|
||||||
return this._rQ;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rQi() {
|
|
||||||
return this._rQi;
|
|
||||||
}
|
|
||||||
|
|
||||||
set rQi(val) {
|
|
||||||
this._rQi = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive Queue
|
// Receive Queue
|
||||||
get rQlen() {
|
|
||||||
return this._rQlen - this._rQi;
|
|
||||||
}
|
|
||||||
|
|
||||||
rQpeek8() {
|
rQpeek8() {
|
||||||
return this._rQ[this._rQi];
|
return this._rQ[this._rQi];
|
||||||
}
|
}
|
||||||
|
|
@ -141,42 +121,47 @@ export default class Websock {
|
||||||
for (let byte = bytes - 1; byte >= 0; byte--) {
|
for (let byte = bytes - 1; byte >= 0; byte--) {
|
||||||
res += this._rQ[this._rQi++] << (byte * 8);
|
res += this._rQ[this._rQi++] << (byte * 8);
|
||||||
}
|
}
|
||||||
return res;
|
return res >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
rQshiftStr(len) {
|
rQshiftStr(len) {
|
||||||
if (typeof(len) === 'undefined') { len = this.rQlen; }
|
|
||||||
let str = "";
|
let str = "";
|
||||||
// Handle large arrays in steps to avoid long strings on the stack
|
// Handle large arrays in steps to avoid long strings on the stack
|
||||||
for (let i = 0; i < len; i += 4096) {
|
for (let i = 0; i < len; i += 4096) {
|
||||||
let part = this.rQshiftBytes(Math.min(4096, len - i));
|
let part = this.rQshiftBytes(Math.min(4096, len - i), false);
|
||||||
str += String.fromCharCode.apply(null, part);
|
str += String.fromCharCode.apply(null, part);
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
rQshiftBytes(len) {
|
rQshiftBytes(len, copy=true) {
|
||||||
if (typeof(len) === 'undefined') { len = this.rQlen; }
|
|
||||||
this._rQi += len;
|
this._rQi += len;
|
||||||
return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
|
if (copy) {
|
||||||
|
return this._rQ.slice(this._rQi - len, this._rQi);
|
||||||
|
} else {
|
||||||
|
return this._rQ.subarray(this._rQi - len, this._rQi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rQshiftTo(target, len) {
|
rQshiftTo(target, len) {
|
||||||
if (len === undefined) { len = this.rQlen; }
|
|
||||||
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
|
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
|
||||||
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
|
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
|
||||||
this._rQi += len;
|
this._rQi += len;
|
||||||
}
|
}
|
||||||
|
|
||||||
rQslice(start, end = this.rQlen) {
|
rQpeekBytes(len, copy=true) {
|
||||||
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
|
if (copy) {
|
||||||
|
return this._rQ.slice(this._rQi, this._rQi + len);
|
||||||
|
} else {
|
||||||
|
return this._rQ.subarray(this._rQi, this._rQi + len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
|
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
|
||||||
// to be available in the receive queue. Return true if we need to
|
// to be available in the receive queue. Return true if we need to
|
||||||
// wait (and possibly print a debug message), otherwise false.
|
// wait (and possibly print a debug message), otherwise false.
|
||||||
rQwait(msg, num, goback) {
|
rQwait(msg, num, goback) {
|
||||||
if (this.rQlen < num) {
|
if (this._rQlen - this._rQi < num) {
|
||||||
if (goback) {
|
if (goback) {
|
||||||
if (this._rQi < goback) {
|
if (this._rQi < goback) {
|
||||||
throw new Error("rQwait cannot backup " + goback + " bytes");
|
throw new Error("rQwait cannot backup " + goback + " bytes");
|
||||||
|
|
@ -190,21 +175,56 @@ export default class Websock {
|
||||||
|
|
||||||
// Send Queue
|
// Send Queue
|
||||||
|
|
||||||
|
sQpush8(num) {
|
||||||
|
this._sQensureSpace(1);
|
||||||
|
this._sQ[this._sQlen++] = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpush16(num) {
|
||||||
|
this._sQensureSpace(2);
|
||||||
|
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpush32(num) {
|
||||||
|
this._sQensureSpace(4);
|
||||||
|
this._sQ[this._sQlen++] = (num >> 24) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 16) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpushString(str) {
|
||||||
|
let bytes = str.split('').map(chr => chr.charCodeAt(0));
|
||||||
|
this.sQpushBytes(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpushBytes(bytes) {
|
||||||
|
for (let offset = 0;offset < bytes.length;) {
|
||||||
|
this._sQensureSpace(1);
|
||||||
|
|
||||||
|
let chunkSize = this._sQbufferSize - this._sQlen;
|
||||||
|
if (chunkSize > bytes.length - offset) {
|
||||||
|
chunkSize = bytes.length - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
|
||||||
|
this._sQlen += chunkSize;
|
||||||
|
offset += chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
if (this._sQlen > 0 && this.readyState === 'open') {
|
if (this._sQlen > 0 && this.readyState === 'open') {
|
||||||
this._websocket.send(this._encodeMessage());
|
this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
|
||||||
this._sQlen = 0;
|
this._sQlen = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
send(arr) {
|
_sQensureSpace(bytes) {
|
||||||
this._sQ.set(arr, this._sQlen);
|
if (this._sQbufferSize - this._sQlen < bytes) {
|
||||||
this._sQlen += arr.length;
|
this.flush();
|
||||||
this.flush();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
sendString(str) {
|
|
||||||
this.send(str.split('').map(chr => chr.charCodeAt(0)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event Handlers
|
// Event Handlers
|
||||||
|
|
@ -283,17 +303,12 @@ export default class Websock {
|
||||||
}
|
}
|
||||||
|
|
||||||
// private methods
|
// private methods
|
||||||
_encodeMessage() {
|
|
||||||
// Put in a binary arraybuffer
|
|
||||||
// according to the spec, you can send ArrayBufferViews with the send method
|
|
||||||
return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to move all the unread data to the start of the queue,
|
// We want to move all the unread data to the start of the queue,
|
||||||
// e.g. compacting.
|
// e.g. compacting.
|
||||||
// The function also expands the receive que if needed, and for
|
// The function also expands the receive que if needed, and for
|
||||||
// performance reasons we combine these two actions to avoid
|
// performance reasons we combine these two actions to avoid
|
||||||
// unneccessary copying.
|
// unnecessary copying.
|
||||||
_expandCompactRQ(minFit) {
|
_expandCompactRQ(minFit) {
|
||||||
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
|
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
|
||||||
// instead of resizing
|
// instead of resizing
|
||||||
|
|
@ -309,7 +324,7 @@ export default class Websock {
|
||||||
// we don't want to grow unboundedly
|
// we don't want to grow unboundedly
|
||||||
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
|
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
|
||||||
this._rQbufferSize = MAX_RQ_GROW_SIZE;
|
this._rQbufferSize = MAX_RQ_GROW_SIZE;
|
||||||
if (this._rQbufferSize - this.rQlen < minFit) {
|
if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
|
||||||
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
|
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -327,25 +342,22 @@ export default class Websock {
|
||||||
}
|
}
|
||||||
|
|
||||||
// push arraybuffer values onto the end of the receive que
|
// push arraybuffer values onto the end of the receive que
|
||||||
_DecodeMessage(data) {
|
_recvMessage(e) {
|
||||||
const u8 = new Uint8Array(data);
|
if (this._rQlen == this._rQi) {
|
||||||
|
// All data has now been processed, this means we
|
||||||
|
// can reset the receive queue.
|
||||||
|
this._rQlen = 0;
|
||||||
|
this._rQi = 0;
|
||||||
|
}
|
||||||
|
const u8 = new Uint8Array(e.data);
|
||||||
if (u8.length > this._rQbufferSize - this._rQlen) {
|
if (u8.length > this._rQbufferSize - this._rQlen) {
|
||||||
this._expandCompactRQ(u8.length);
|
this._expandCompactRQ(u8.length);
|
||||||
}
|
}
|
||||||
this._rQ.set(u8, this._rQlen);
|
this._rQ.set(u8, this._rQlen);
|
||||||
this._rQlen += u8.length;
|
this._rQlen += u8.length;
|
||||||
}
|
|
||||||
|
|
||||||
_recvMessage(e) {
|
if (this._rQlen - this._rQi > 0) {
|
||||||
this._DecodeMessage(e.data);
|
|
||||||
if (this.rQlen > 0) {
|
|
||||||
this._eventHandlers.message();
|
this._eventHandlers.message();
|
||||||
if (this._rQlen == this._rQi) {
|
|
||||||
// All data has now been processed, this means we
|
|
||||||
// can reset the receive queue.
|
|
||||||
this._rQlen = 0;
|
|
||||||
this._rQi = 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.Debug("Ignoring empty message");
|
Log.Debug("Ignoring empty message");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
public/novnc/package.json
Normal file
1
public/novnc/package.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{ "version": "1.5.0" }
|
||||||
963
public/scripts/agent-desktop-0.0.2-min.js
vendored
963
public/scripts/agent-desktop-0.0.2-min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -155,7 +155,6 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
|
||||||
if (Msg[1] == 1) { obj.ProcessCopyRectMsg(Msg[2]); }
|
if (Msg[1] == 1) { obj.ProcessCopyRectMsg(Msg[2]); }
|
||||||
else if (Msg[1] == 2) { obj.Canvas.drawImage(Msg[2], obj.rotX(Msg[3], Msg[4]), obj.rotY(Msg[3], Msg[4])); delete Msg[2]; }
|
else if (Msg[1] == 2) { obj.Canvas.drawImage(Msg[2], obj.rotX(Msg[3], Msg[4]), obj.rotY(Msg[3], Msg[4])); delete Msg[2]; }
|
||||||
obj.PendingOperations.splice(i, 1);
|
obj.PendingOperations.splice(i, 1);
|
||||||
delete Msg;
|
|
||||||
obj.TilesDrawn++;
|
obj.TilesDrawn++;
|
||||||
if ((obj.TilesDrawn == obj.tilesReceived) && (obj.KillDraw < obj.TilesDrawn)) { obj.KillDraw = obj.TilesDrawn = obj.tilesReceived = 0; }
|
if ((obj.TilesDrawn == obj.tilesReceived) && (obj.KillDraw < obj.TilesDrawn)) { obj.KillDraw = obj.TilesDrawn = obj.tilesReceived = 0; }
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -221,12 +220,16 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
|
||||||
if ((cmd == 3) || (cmd == 4) || (cmd == 7)) { X = (view[4] << 8) + view[5]; Y = (view[6] << 8) + view[7]; }
|
if ((cmd == 3) || (cmd == 4) || (cmd == 7)) { X = (view[4] << 8) + view[5]; Y = (view[6] << 8) + view[7]; }
|
||||||
if (obj.debugmode > 2) { console.log('CMD', cmd, cmdsize, X, Y); }
|
if (obj.debugmode > 2) { console.log('CMD', cmd, cmdsize, X, Y); }
|
||||||
|
|
||||||
|
// Fix for view being too large for String.fromCharCode.apply()
|
||||||
|
var chunkSize = 10000;
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < view.length; i += chunkSize) { result += String.fromCharCode.apply(null, view.slice(i, i + chunkSize)); }
|
||||||
// Record the command if needed
|
// Record the command if needed
|
||||||
if (obj.recordedData != null) {
|
if (obj.recordedData != null) {
|
||||||
if (cmdsize > 65000) {
|
if (cmdsize > 65000) {
|
||||||
obj.recordedData.push(recordingEntry(2, 1, obj.shortToStr(27) + obj.shortToStr(8) + obj.intToStr(cmdsize) + obj.shortToStr(cmd) + obj.shortToStr(0) + obj.shortToStr(0) + obj.shortToStr(0) + String.fromCharCode.apply(null, view)));
|
obj.recordedData.push(recordingEntry(2, 1, obj.shortToStr(27) + obj.shortToStr(8) + obj.intToStr(cmdsize) + obj.shortToStr(cmd) + obj.shortToStr(0) + obj.shortToStr(0) + obj.shortToStr(0) + result));
|
||||||
} else {
|
} else {
|
||||||
obj.recordedData.push(recordingEntry(2, 1, String.fromCharCode.apply(null, view)));
|
obj.recordedData.push(recordingEntry(2, 1, result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -575,7 +578,7 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
|
||||||
var Delta = 0;
|
var Delta = 0;
|
||||||
if (Action == obj.KeyAction.UP || Action == obj.KeyAction.DOWN) {
|
if (Action == obj.KeyAction.UP || Action == obj.KeyAction.DOWN) {
|
||||||
if (event.which) { ((event.which == 1) ? (Button = obj.MouseButton.LEFT) : ((event.which == 2) ? (Button = obj.MouseButton.MIDDLE) : (Button = obj.MouseButton.RIGHT))); }
|
if (event.which) { ((event.which == 1) ? (Button = obj.MouseButton.LEFT) : ((event.which == 2) ? (Button = obj.MouseButton.MIDDLE) : (Button = obj.MouseButton.RIGHT))); }
|
||||||
else if (event.button) { ((event.button == 0) ? (Button = obj.MouseButton.LEFT) : ((event.button == 1) ? (Button = obj.MouseButton.MIDDLE) : (Button = obj.MouseButton.RIGHT))); }
|
else if (typeof event.button == 'number') { ((event.button == 0) ? (Button = obj.MouseButton.LEFT) : ((event.button == 1) ? (Button = obj.MouseButton.MIDDLE) : (Button = obj.MouseButton.RIGHT))); }
|
||||||
}
|
}
|
||||||
else if (Action == obj.KeyAction.SCROLL) {
|
else if (Action == obj.KeyAction.SCROLL) {
|
||||||
if (event.detail) { Delta = (-1 * (event.detail * 120)); } else if (event.wheelDelta) { Delta = (event.wheelDelta * 3); }
|
if (event.detail) { Delta = (-1 * (event.detail * 120)); } else if (event.wheelDelta) { Delta = (event.wheelDelta * 3); }
|
||||||
|
|
|
||||||
2
public/scripts/agent-rdp-0.0.1-min.js
vendored
2
public/scripts/agent-rdp-0.0.1-min.js
vendored
File diff suppressed because one or more lines are too long
2
public/scripts/amt-desktop-0.0.2-min.js
vendored
2
public/scripts/amt-desktop-0.0.2-min.js
vendored
File diff suppressed because one or more lines are too long
2
public/scripts/amt-ider-ws-0.0.1-min.js
vendored
2
public/scripts/amt-ider-ws-0.0.1-min.js
vendored
File diff suppressed because one or more lines are too long
2
public/scripts/amt-redir-ws-0.1.0-min.js
vendored
2
public/scripts/amt-redir-ws-0.1.0-min.js
vendored
File diff suppressed because one or more lines are too long
2
public/scripts/amt-terminal-0.0.2-min.js
vendored
2
public/scripts/amt-terminal-0.0.2-min.js
vendored
File diff suppressed because one or more lines are too long
2
public/scripts/amt-wsman-0.2.0-min.js
vendored
2
public/scripts/amt-wsman-0.2.0-min.js
vendored
File diff suppressed because one or more lines are too long
7
public/scripts/bootstrap-min.js
vendored
Normal file
7
public/scripts/bootstrap-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6314
public/scripts/bootstrap.js
vendored
Normal file
6314
public/scripts/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
public/scripts/bootstrap.js.map
Normal file
1
public/scripts/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
1
public/scripts/bootstrap.min.js.map
Normal file
1
public/scripts/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue