diff --git a/LICENSE b/LICENSE index cf8faf5a..85c3dcae 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier 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"); you may not use this file except in compliance with the License. diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 475667fe..f5b9d0de 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -594,12 +594,14 @@ + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..57790a24 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +## Supported Versions + +Any version of MeshCentral 1.x.x is supported. + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | +| < 1.0 | :x: | + +## Reporting a Vulnerability + +Please report any concerns or security issue to Ylian Saint-Hilaire (ylianst@gmail.com). If needed, use my PGP key below. + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.56 + +mQMuBF2gC4sRCAClFNvMCCVW3ego3UHBQ6LhSenJfaZYhvn8gaGuemSQxqTI6bla +BTAv3aMtQnvqlSuadMMegb+FO6hnaQMlGvpVA1qpkSzgrPS5HrBD3H33J2Nj3i93 +ZpDPpxdI0ehCj6IJPnl0GxGbpKIN8YpJUFl44wv1lMRFI1lgyb+dCoO60irYdNQB +PV85BI+DwPfOBFHunwR78nqMvpvsk9HaeHjEP7oXr952/7EazUowZsMlEfkYnw5S ++tLfpCoY3QWkektpJP40nMJSKQdV2NEuED99doA0X+7P1vsvFFFyMH69dnU2uSay +XCHpkAbntBy0BGmtF1RnTcOMv2V/LPXnlMdvAQCbmLQzNra3r163tcdRY0jSs+pZ +1L3w5tHNj2dzhfpa7wf/SIuds6QTr2LCN6miLoSVCRMMpT7d771b16GwQqWEXzN2 ++h7dYqrssHPOa8FSUrPerz0+0eFcbMSm5/L/4KXWXoQthURv8aMP9E0iVoUYaaKB +7U+5vFEZbpoOZyZmTAjXQMSNZCft0azA82Q+G85euyicWtMv48yNVzUhkdh+M2ud +ohkXX2Aor1TqpBJoIeWke7j9D+Bo+lu61zPRx5ed9teUeLJCwqNEjlE+6gre5kxF +PoreAtn59QYcBIpzQEWVMbNFlDAR4jMyqIoKCGfBPiRw2V+kunbzqiGQEglIFfOt +6sTN/+CJh0ei976VDmE0Z1kMN+CNLgIjIw8fl02V9QgAnHcpqtVUxR4dbGOhVDq5 +lWv+K75QQlWyXC2k+KboXcaCvH0WZEBACYzO0CfrZ5hP9BSkbj5usSUVGGHwEFAJ +t+/04KVY71fW281Ej5kGNaIKxeKsx6+hMo+UXb5ZM+6fANNNxs1cK95sTH6PjkyB +tsKxLoa3CV2v9mSE5JiKKt74R9nXVo7PXf6DizwAU2l30Lb6y6y0OdXdCCPAG8Ij +FrMgPu5MtjgsO5DnkZfUqDPWHhOgEPyOh3Ho+pvDhNYh5cm2eLQ8g5orzs2FHwbZ +DpAHwCdqrlcpBlKJ4W/MZdf1fg2PjqaTWm7ZFiGr91P0F6kltTLWbVKTjLdS0T+D +L7QnWWxpYW4gU2FpbnQtSGlsYWlyZSA8eWxpYW5zdEBnbWFpbC5jb20+iF4EExEI +AAYFAl2gC4sACgkQg7j/r4DH+kD/3gD+MRedlM53VzOtNOpS6mqDAxj1aWP90HN0 +AqO6zuCTyGgBAJlunLFKH8IUetmQOhiohB8HVhdm/q4lKRDV7sHdplDyuMwEXaAL +ixACAJSU/sCV87he4oZUKzg2/IGl3QoDSbTCOd04dE1IjPjjHbi8t9M7Qau55aM8 +ypFEsc7zMslL8Fc78EejrKmM3zsB/RU9XWFyrbQwRbaK6OHeEHC2E3AFaG0p09c6 +d0kZloHuWyEsm5a/3PpbIM1eP9IESJXWCc+bQQt6DxLKHLmkKMwB/icWMg8uMJlx +aady8TEq7LH5oFVKsglnwuN1nIkecrf77TVkEqTjIxS6TiOup6zOnioFNKLYBAH0 +WUnJEYFvx4OIXgQYEQgABgUCXaALiwAKCRCDuP+vgMf6QGFTAQCUj2gGwsFlN0eR +Wowv4eLcc3FwQ+lBElUctKg8vNFb0gD/ZWVWsWwKerNgNnf7RGD9mt8G2CKvdgGG +oZ2hPP2gU9w= +=roW4 +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/agents/MeshAgentOSXPackager.zip b/agents/MeshAgentOSXPackager.zip index c4936a66..4248a4eb 100644 Binary files a/agents/MeshAgentOSXPackager.zip and b/agents/MeshAgentOSXPackager.zip differ diff --git a/agents/MeshCentralRouter.exe b/agents/MeshCentralRouter.exe index 57a4e352..828675dc 100644 Binary files a/agents/MeshCentralRouter.exe and b/agents/MeshCentralRouter.exe differ diff --git a/agents/MeshCmd.exe b/agents/MeshCmd.exe index 57c590aa..00b704aa 100644 Binary files a/agents/MeshCmd.exe and b/agents/MeshCmd.exe differ diff --git a/agents/MeshCmd64.exe b/agents/MeshCmd64.exe index fde90d0a..e4af314f 100644 Binary files a/agents/MeshCmd64.exe and b/agents/MeshCmd64.exe differ diff --git a/agents/MeshCmdARM64.exe b/agents/MeshCmdARM64.exe index 6f4f5e6c..83f1079a 100644 Binary files a/agents/MeshCmdARM64.exe and b/agents/MeshCmdARM64.exe differ diff --git a/agents/MeshService.exe b/agents/MeshService.exe index b6b4a387..e3aec7ab 100644 Binary files a/agents/MeshService.exe and b/agents/MeshService.exe differ diff --git a/agents/MeshService64.exe b/agents/MeshService64.exe index bb5ed095..540fed64 100644 Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ diff --git a/agents/MeshServiceARM64.exe b/agents/MeshServiceARM64.exe index c53d7610..ee191f79 100644 Binary files a/agents/MeshServiceARM64.exe and b/agents/MeshServiceARM64.exe differ diff --git a/agents/agent-translations.json b/agents/agent-translations.json index da22d071..554c68e5 100644 --- a/agents/agent-translations.json +++ b/agents/agent-translations.json @@ -682,5 +682,67 @@ ], "statusDescription": "Jelenlegi agent állapota", "description": "Kattintson a Telepítés vagy Eltávolítás gombokra a Távfelügyeleti alkalmazás telepítéséhez vagy eltávolításához. Telepítés után ez az alkalmazás a háttérben fut, lehetővé téve, hogy a számítógépet egy távoli rendszergazda kezelje." + }, + "ca": { + "agent": "Agent", + "agentVersion": "Nova versió", + "group": "Grup de dispositius", + "url": "URL del servidor", + "meshName": "Nom del grup", + "meshId": "Identificador de grup", + "serverId": "Identificador del servidor", + "setup": "Configuració", + "update": "Actualització", + "install": "Instal·lar", + "uninstall": "Desinstal·la", + "connect": "Connecta't", + "disconnect": "Desconnecta", + "cancel": "Cancel · lar", + "close": "Tanca", + "pressok": "Premeu D'acord per desconnectar", + "elevation": "Es necessiten permisos elevats per instal·lar/desinstal·lar aquest programari.", + "sudo": "Si us plau, torna-ho a provar amb sudo.", + "ctrlc": "Premeu Ctrl-C per sortir.", + "commands": "Podeu executar la versió de text des de la línia d'ordres amb les següents ordres", + "graphicalerror": "La versió gràfica d'aquest instal·lador no pot executar-se en aquest sistema", + "zenity": "Proveu d'instal·lar/actualitzar Zenity i torneu a executar-lo", + "status": [ + "NO ESTÀ INSTAL · LAT", + "CÓRRER", + "NO CORRE" + ], + "statusDescription": "Estat actual de l'agent", + "description": "Feu clic als botons següents per instal·lar o desinstal·lar aquest programari de gestió remota. Quan s'instal·la, aquest programari s'executa en segon pla i permet que aquest ordinador sigui gestionat i controlat per un administrador remot." + }, + "uk": { + "agent": "Агент", + "agentVersion": "Нова Версія", + "group": "Група Пристроїв", + "url": "URL Сервера", + "meshName": "Ім'я Групи", + "meshId": "Ідентифікатор групи", + "serverId": "Ідентифікатор серверу", + "setup": "Налаштувати", + "update": "Оновлення", + "install": "Інсталювати", + "uninstall": "Видалити", + "connect": "Підключитися", + "disconnect": "Відключити", + "cancel": "Скасувати", + "close": "Закрити", + "pressok": "Натисніть OK, щоб від'єднатися", + "elevation": "Для інсталяції/деінсталяції цього програмного забезпечення потрібні підвищені дозволи.", + "sudo": "Будь ласка, спробуйте ще раз за допомогою sudo.", + "ctrlc": "Натисніть Ctrl-C, щоб вийти", + "commands": "Ви можете запустити текстову версію з командного рядка за допомогою таких команд", + "graphicalerror": "Графічна версія цього інсталятора не може працювати в цій системі", + "zenity": "Спробуйте встановити/оновити Zenity та запустіть наново", + "status": [ + "НЕ ВСТАНОВЛЕНО", + "ВИКОНУЄТЬСЯ", + "НЕ ПРАЦЮЄ" + ], + "statusDescription": "Поточний Статус Агента", + "description": "Клікнути кнопки нижче, щоб інсталювати або видалити це програмне забезпечення для віддаленого керування. Після інсталювання ця програма працює у фоновому режимі, що дозволяє віддаленому адміністратору керувати цим комп'ютером." } } \ No newline at end of file diff --git a/agents/meshagent_android.apk b/agents/meshagent_android.apk index 0663a1f0..0b525a3b 100644 Binary files a/agents/meshagent_android.apk and b/agents/meshagent_android.apk differ diff --git a/agents/meshcmd.js b/agents/meshcmd.js index ac782e8c..1142c31a 100644 --- a/agents/meshcmd.js +++ b/agents/meshcmd.js @@ -588,7 +588,7 @@ function run(argv) { } amtMei.getProvisioningState(function (result) { if (result) { mestate.ProvisioningState = 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.getMACAddresses(function (result) { if (result) { mestate.mac = result; } }); amtMei.getLanInterfaceSettings(0, function (result) { if (result) { mestate.net0 = result; } }); diff --git a/agents/meshcore.js b/agents/meshcore.js index 8461de62..5a7faaf1 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -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 try { 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) { } - if (writtenSize != actualSize) { try { require('win-registry').WriteKey(require('win-registry').HKEY.LocalMachine, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MeshCentralAgent', 'EstimatedSize', actualSize); } catch (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) { } + 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) { } // 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) { } if (meshCheck && require('win-bcd').isSafeModeService && !require('win-bcd').isSafeModeService(svcname)) { require('win-bcd').enableSafeModeService(svcname); } } 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') { @@ -654,50 +665,40 @@ var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ? // Get the operating system description string try { require('os').name().then(function (v) { meshCoreObj.osdesc = v; meshCoreObjChanged(); }); } catch (ex) { } -// Get Volumes and BitLocker if Windows -try { - if (process.platform == 'win32'){ - if (require('computer-identifiers').volumes_promise != null){ - var p = require('computer-identifiers').volumes_promise(); - p.then(function (res){ - meshCoreObj.volumes = res; - meshCoreObjChanged(); - }); - }else if (require('computer-identifiers').volumes != null){ - meshCoreObj.volumes = require('computer-identifiers').volumes(); - meshCoreObjChanged(); - } - } -} catch(e) { } - // 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 { var userSession = require('user-sessions'); - userSession.on('changed', function onUserSessionChanged() { - 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.on('changed', function () { onUserSessionChanged(null, false); }); userSession.emit('changed'); - //userSession.on('locked', function (user) { sendConsoleText('[' + (user.Domain ? user.Domain + '\\' : '') + user.Username + '] has LOCKED the desktop'); }); - //userSession.on('unlocked', function (user) { sendConsoleText('[' + (user.Domain ? user.Domain + '\\' : '') + user.Username + '] has UNLOCKED the desktop'); }); + userSession.on('locked', function (user) { if(user != undefined && user != null) { onUserSessionChanged(user, true); } }); + userSession.on('unlocked', function (user) { if(user != undefined && user != null) { onUserSessionChanged(user, false); } }); } catch (ex) { } var meshServerConnectionState = 0; @@ -1174,6 +1175,8 @@ function handleServerCommand(data) { tunnel.soptions = data.soptions; tunnel.consentTimeout = (tunnel.soptions && tunnel.soptions.consentTimeout) ? tunnel.soptions.consentTimeout : 30; 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.tcpport = data.tcpport; tunnel.udpaddr = data.udpaddr; @@ -1535,7 +1538,7 @@ function handleServerCommand(data) { } case 'runcommands': { if (mesh.cmdchild != null) { sendConsoleText("Run commands can't execute, already busy."); break; } - sendConsoleText("Run commands (" + data.runAsUser + "): " + data.cmds); + if (!data.reply) sendConsoleText("Run commands (" + data.runAsUser + "): " + data.cmds); // data.runAsUser: 0=Agent,1=UserOrAgent,2=UserOnly var options = {}; @@ -1556,7 +1559,15 @@ function handleServerCommand(data) { mesh.cmdchild.stdout.on('data', function (c) { replydata += c.toString(); }); mesh.cmdchild.stderr.on('data', function (c) { replydata += c.toString(); }); mesh.cmdchild.stdin.write(data.cmds + '\r\nexit\r\n'); - mesh.cmdchild.on('exit', function () { sendConsoleText(replydata); sendConsoleText("Run commands completed."); delete mesh.cmdchild; }); + mesh.cmdchild.on('exit', function () { + if (data.reply) { + mesh.SendCommand({ action: 'msg', type: 'runcommands', result: replydata, sessionid: data.sessionid, responseid: data.responseid }); + } else { + sendConsoleText(replydata); + sendConsoleText("Run commands completed."); + } + delete mesh.cmdchild; + }); } else if (data.type == 2) { // Windows Powershell mesh.cmdchild = require('child_process').execFile(process.env['windir'] + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', ['powershell', '-noprofile', '-nologo', '-command', '-'], options); @@ -1564,16 +1575,32 @@ function handleServerCommand(data) { mesh.cmdchild.stdout.on('data', function (c) { replydata += c.toString(); }); mesh.cmdchild.stderr.on('data', function (c) { replydata += c.toString(); }); mesh.cmdchild.stdin.write(data.cmds + '\r\nexit\r\n'); - mesh.cmdchild.on('exit', function () { sendConsoleText(replydata); sendConsoleText("Run commands completed."); delete mesh.cmdchild; }); + mesh.cmdchild.on('exit', function () { + if (data.reply) { + mesh.SendCommand({ action: 'msg', type: 'runcommands', result: replydata, sessionid: data.sessionid, responseid: data.responseid }); + } else { + sendConsoleText(replydata); + sendConsoleText("Run commands completed."); + } + delete mesh.cmdchild; + }); } } else if (data.type == 3) { // Linux shell mesh.cmdchild = require('child_process').execFile('/bin/sh', ['sh'], options); mesh.cmdchild.descriptorMetadata = 'UserCommandsShell'; 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.on('exit', function () { sendConsoleText(replydata); sendConsoleText("Run commands completed."); delete mesh.cmdchild; }); + mesh.cmdchild.on('exit', function () { + if (data.reply) { + mesh.SendCommand({ action: 'msg', type: 'runcommands', result: replydata, sessionid: data.sessionid, responseid: data.responseid }); + } else { + sendConsoleText(replydata); + sendConsoleText("Run commands completed."); + } + delete mesh.cmdchild; + }); } break; } @@ -1875,115 +1902,68 @@ function getSystemInformation(func) { if (results.hardware.windows.osinfo) { delete results.hardware.windows.osinfo.Node; } if (results.hardware.windows.partitions) { for (var i in results.hardware.windows.partitions) { delete results.hardware.windows.partitions[i].Node; } } } catch (ex) { } - if (!results.hardware.identifiers['bios_serial']) { - try { - var values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_Bios", ['SerialNumber']); - results.hardware.identifiers['bios_serial'] = values[0]['SerialNumber']; - } catch (ex) { } - } - if (!results.hardware.identifiers['bios_mode']) { - try { - results.hardware.identifiers['bios_mode'] = 'Legacy'; - for (var i in results.hardware.windows.partitions) { - if (results.hardware.windows.partitions[i].Description=='GPT: System') { - results.hardware.identifiers['bios_mode'] = 'UEFI'; - } - } - } catch (ex) { results.hardware.identifiers['bios_mode'] = 'Legacy'; } - } - if (!results.hardware.tpm) { - IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }; - try { - var values = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftTpm', "SELECT * FROM Win32_Tpm", ['IsActivated_InitialValue','IsEnabled_InitialValue','IsOwned_InitialValue','ManufacturerId','ManufacturerVersion','SpecVersion']); - if(values[0]) { - results.hardware.tpm = { - SpecVersion: values[0].SpecVersion.split(",")[0], - ManufacturerId: IntToStr(values[0].ManufacturerId).replace(/[^\x00-\x7F]/g, ""), - ManufacturerVersion: values[0].ManufacturerVersion, - IsActivated: values[0].IsActivated_InitialValue, - IsEnabled: values[0].IsEnabled_InitialValue, - IsOwned: values[0].IsOwned_InitialValue, - } - } - } catch (ex) { } + if (x.LastBootUpTime) { // detect windows uptime + var thedate = { + year: parseInt(x.LastBootUpTime.substring(0, 4)), + month: parseInt(x.LastBootUpTime.substring(4, 6)) - 1, // Months are 0-based in JavaScript (0 - January, 11 - December) + day: parseInt(x.LastBootUpTime.substring(6, 8)), + hours: parseInt(x.LastBootUpTime.substring(8, 10)), + minutes: parseInt(x.LastBootUpTime.substring(10, 12)), + seconds: parseInt(x.LastBootUpTime.substring(12, 14)), + }; + var thelastbootuptime = new Date(thedate.year, thedate.month, thedate.day, thedate.hours, thedate.minutes, thedate.seconds); + meshCoreObj.lastbootuptime = thelastbootuptime.getTime(); // store the last boot up time in coreinfo for columns + meshCoreObjChanged(); + var nowtime = new Date(); + var differenceInMilliseconds = Math.abs(thelastbootuptime - nowtime); + if (differenceInMilliseconds < 300000) { // computer uptime less than 5 minutes + MeshServerLogEx(159, [thelastbootuptime.toString()], "Device Powered On", null); + } } } if(results.hardware && results.hardware.linux) { - if (!results.hardware.identifiers['bios_serial']) { - try { - if (require('fs').statSync('/sys/class/dmi/id/product_serial').isFile()){ - results.hardware.identifiers['bios_serial'] = require('fs').readFileSync('/sys/class/dmi/id/product_serial').toString().trim(); - } - } catch (ex) { } - } - if (!results.hardware.identifiers['bios_mode']) { - try { - results.hardware.identifiers['bios_mode'] = (require('fs').statSync('/sys/firmware/efi').isDirectory() ? 'UEFI': 'Legacy'); - } catch (ex) { results.hardware.identifiers['bios_mode'] = 'Legacy'; } - } - if (!results.hardware.tpm) { - IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }; - try { - if (require('fs').statSync('/sys/class/tpm/tpm0').isDirectory()){ - results.hardware.tpm = { - SpecVersion: require('fs').readFileSync('/sys/class/tpm/tpm0/tpm_version_major').toString().trim() - } - } - } catch (ex) { } - } - if(!results.hardware.linux.LastBootUpTime) { - try { - var child = require('child_process').execFile('/usr/bin/uptime', ['', '-s']); // must include blank value at begining for some reason? - child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); - child.stderr.on('data', function () { }); - child.waitExit(); - results.hardware.linux.LastBootUpTime = child.stdout.str.trim(); - } catch (ex) { } - } - } - if(process.platform=='darwin'){ - try { - var child = require('child_process').execFile('/usr/sbin/sysctl', ['', 'kern.boottime']); // must include blank value at begining for some reason? - child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); - child.stderr.on('data', function () { }); - child.waitExit(); - const timestampMatch = /\{ sec = (\d+), usec = \d+ \}/.exec(child.stdout.str.trim()); - if(!results.hardware.darwin){ - results.hardware.darwin = { LastBootUpTime: parseInt(timestampMatch[1]) }; - }else{ - results.hardware.darwin.LastBootUpTime = parseInt(timestampMatch[1]); + if(results.hardware.linux.LastBootUpTime) { + var thelastbootuptime = new Date(results.hardware.linux.LastBootUpTime); + meshCoreObj.lastbootuptime = thelastbootuptime.getTime(); // store the last boot up time in coreinfo for columns + meshCoreObjChanged(); + var nowtime = new Date(); + var differenceInMilliseconds = Math.abs(thelastbootuptime - nowtime); + if (differenceInMilliseconds < 300000) { // computer uptime less than 5 minutes + MeshServerLogEx(159, [thelastbootuptime.toString()], "Device Powered On", null); } - } catch (ex) { } + } } + if(results.hardware && results.hardware.darwin){ + if(results.hardware.darwin.LastBootUpTime) { + var thelastbootuptime = new Date(results.hardware.darwin.LastBootUpTime * 1000); // must times by 1000 even tho timestamp is correct? + meshCoreObj.lastbootuptime = thelastbootuptime.getTime(); // store the last boot up time in coreinfo for columns + meshCoreObjChanged(); + var nowtime = new Date(); + var differenceInMilliseconds = Math.abs(thelastbootuptime - nowtime); + if (differenceInMilliseconds < 300000) { // computer uptime less than 5 minutes + MeshServerLogEx(159, [thelastbootuptime.toString()], "Device Powered On", null); + } + } + } results.hardware.agentvers = process.versions; results.hardware.network = { dns: require('os').dns() }; replaceSpacesWithUnderscoresRec(results); var hasher = require('SHA384Stream').create(); - // results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); - // func(results); - // On Windows platforms, get volume information - Needs more testing. if (process.platform == 'win32') { 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) { - results.hardware.windows.volumes = res; + results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(res); results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); func(results); }); } - else if (require('computer-identifiers').volumes != null) - { - results.hardware.windows.volumes = require('computer-identifiers').volumes(); - results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); - func(results); - } else { results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); @@ -2005,11 +1985,10 @@ function getDirectoryInfo(reqpath) { if (((reqpath == undefined) || (reqpath == '')) && (process.platform == 'win32')) { // List all the drives in the root, or the root itself var results = null; - try { results = fs.readDrivesSync(); } catch (ex) { } // TODO: Anyway to get drive total size and free space? Could draw a progress bar. + try { results = fs.readDrivesSync(); } catch (ex) { } if (results != null) { for (var i = 0; i < results.length; ++i) { - var drive = { n: results[i].name, t: 1 }; - if (results[i].type == 'REMOVABLE') { drive.dt = 'removable'; } // TODO: See if this is USB/CDROM or something else, we can draw icons. + var drive = { n: results[i].name, t: 1, dt: results[i].type, s: (results[i].size ? results[i].size : 0), f: (results[i].free ? results[i].free : 0) }; response.dir.push(drive); } } @@ -2336,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) { // FAILED to connect terminal @@ -2388,7 +2420,7 @@ function terminal_promise_connection_resolved(term) if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 2)) { // User Notifications is required - var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.username); + var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.realname ? this.ws.httprequest.realname : this.ws.httprequest.username); var notifyTitle = "MeshCentral"; if (this.ws.httprequest.soptions != null) { @@ -2648,13 +2680,111 @@ function kvm_tunnel_consentpromise_closehandler() 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) { - // User Consent Denied/Failed - this.ws._consentpromise = null; - MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - this.ws = null; + if (this.ws) { + if(this.ws.httprequest){ // User Consent Denied + MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + } else { } // Connection was closed server side, maybe log some messages somewhere? + this.ws._consentpromise = null; + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws = null; + } else { } // no websocket, maybe log some messages somewhere? } function kvm_consentpromise_resolved(always) { @@ -2724,6 +2854,67 @@ function kvm_consentpromise_resolved(always) 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) { if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); } @@ -2749,11 +2940,14 @@ function files_consentpromise_resolved(always) } function files_consentpromise_rejected(e) { - // User Consent Denied/Failed - this.ws._consentpromise = null; - MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - this.ws = null; + if (this.ws) { + if(this.ws.httprequest){ // User Consent Denied + MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + } else { } // Connection was closed server side, maybe log some messages somewhere? + this.ws._consentpromise = null; + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws = null; + } else { } // no websocket, maybe log some messages somewhere? } function files_tunnel_endhandler() { @@ -2834,6 +3028,12 @@ function onTunnelData(data) 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 (!require('win-terminal').PowerShellCapable() && (this.httprequest.protocol == 6 || this.httprequest.protocol == 9)) { @@ -2850,71 +3050,31 @@ function onTunnelData(data) this.end = terminal_end; // Perform User-Consent if needed. - if (this.httprequest.consent && (this.httprequest.consent & 16)) - { - this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); - var consentMessage = currentTranslation['terminalConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); - var consentTitle = 'MeshCentral'; - - if (this.httprequest.soptions != null) - { - if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } - if (this.httprequest.soptions.consentMsgTerminal != null) { consentMessage = this.httprequest.soptions.consentMsgTerminal.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } - } - 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.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) { - // Denied - MeshServerLogEx(28, null, "Local user rejected remote terminal request (" + this.retPromise.that.httprequest.remoteaddr + ")", this.retPromise.that.httprequest); - this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - this.retPromise._consent = null; - this.retPromise._rej(e.toString()); + 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 + if (this.httprequest.consentAutoAcceptIfNoUser) { + var p = require('user-sessions').enumerateUsers(); + p.sessionid = this.httprequest.sessionid; + p.ws = this; + p.then(function (u) { + var v = []; + 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 + this.ws.httprequest.tpromise._res(); + } else { + // User is present so we still need consent + terminal_consent_ask(this.ws); + } }); - } - else - { + } else { + terminal_consent_ask(this); + } + } else { // User-Consent is not required, so just resolve this promise this.httprequest.tpromise._res(); } - - this.httprequest.tpromise.then(terminal_promise_consent_resolved, terminal_promise_consent_rejected); } else if (this.httprequest.protocol == 2) @@ -2938,7 +3098,22 @@ function onTunnelData(data) 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 MacOS, Wake up device with caffeinate + if(process.platform == 'darwin'){ + try { + var options = {}; + try { options.uid = require('user-sessions').consoleUid(); } catch (ex) { } + options.type = require('child_process').SpawnTypes.TERM; + var replydata = ""; + var cmdchild = require('child_process').execFile('/usr/bin/caffeinate', ['caffeinate', '-u', '-t', '10'], options); + cmdchild.descriptorMetadata = 'UserCommandsShell'; + cmdchild.stdout.on('data', function (c) { replydata += c.toString(); }); + cmdchild.stderr.on('data', function (c) { replydata + c.toString(); }); + cmdchild.on('exit', function () { delete cmdchild; }); + } catch(err) { } + } // Remote desktop using native pipes this.httprequest.desktop = { state: 0, kvm: mesh.getRemoteDesktopStream(tsid), tunnel: this }; this.httprequest.desktop.kvm.parent = this.httprequest.desktop; @@ -2995,117 +3170,33 @@ function onTunnelData(data) } // Perform notification if needed. Toast messages may not be supported on all platforms. - if (this.httprequest.consent && (this.httprequest.consent & 8)) - { - // User Consent Prompt is required - // Send a console message back using the console channel, "\n" is supported. - this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); - var consentMessage = currentTranslation['desktopConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); - var consentTitle = 'MeshCentral'; - if (this.httprequest.soptions != null) - { - if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } - if (this.httprequest.soptions.consentMsgDesktop != null) { consentMessage = this.httprequest.soptions.consentMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } + if (this.httprequest.consent && (this.httprequest.consent & 8)) { + + // User asked for consent but now we check if can auto accept if no user is present + if (this.httprequest.consentAutoAcceptIfNoUser) { + // Get list of users to check if we any actual users logged in, and if users logged in, we still need consent + var p = require('user-sessions').enumerateUsers(); + p.sessionid = this.httprequest.sessionid; + p.ws = this; + p.then(function (u) { + var v = []; + 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; - 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 - { + } else { // User Consent Prompt is not required - if (this.httprequest.consent && (this.httprequest.consent & 1)) - { - // 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); - } + kvm_consent_ok(this); } this.removeAllListeners('data'); @@ -3127,6 +3218,12 @@ function onTunnelData(data) 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 if (this.httprequest.userid != null) { var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); @@ -3149,69 +3246,31 @@ function onTunnelData(data) // Perform notification if needed. Toast messages may not be supported on all platforms. if (this.httprequest.consent && (this.httprequest.consent & 32)) { - // User Consent Prompt is required - // Send a console message back using the console channel, "\n" is supported. - this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); - var consentMessage = currentTranslation['fileConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); - var consentTitle = 'MeshCentral'; - - if (this.httprequest.soptions != null) - { - if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } - if (this.httprequest.soptions.consentMsgFiles != null) { consentMessage = this.httprequest.soptions.consentMsgFiles.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } - } - var pr; - 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.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) { } + // User asked for consent so now we check if we can auto accept if no user is present/loggedin + if (this.httprequest.consentAutoAcceptIfNoUser) { + var p = require('user-sessions').enumerateUsers(); + p.sessionid = this.httprequest.sessionid; + p.ws = this; + p.then(function (u) { + var v = []; + 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 + // User Consent Prompt is not required + files_consent_ok(this.ws); + } else { + // User is present so we still need consent + files_consent_ask(this.ws); + } + }); } 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 @@ -3291,6 +3350,13 @@ function onTunnelData(data) } break; } + case 'open': { + // Open the local file/folder on the users desktop + if (cmd.path) { + MeshServerLogEx(20, [cmd.path], "Opening: " + cmd.path, cmd); + openFileOnDesktop(cmd.path); + } + } case 'markcoredump': { // If we are asking for the coredump file, set the right path. var coreDumpPath = null; @@ -3350,7 +3416,7 @@ function onTunnelData(data) if (cmd.sub == 'startack') { sendNextBlock = ((typeof cmd.ack == 'number') ? cmd.ack : 8); } else if (cmd.sub == 'stop') { delete this.filedownload; } else if (cmd.sub == 'ack') { sendNextBlock = 1; } } // Send the next download block(s) - while (sendNextBlock > 0) { + if (sendNextBlock > 0) { sendNextBlock--; var buf = Buffer.alloc(16384); var len = fs.readSync(this.filedownload.f, buf, 4, 16380, null); @@ -3459,6 +3525,24 @@ function onTunnelData(data) this.zip.on('progress', require('events').moderated(function (name, p) { this.xws.write(Buffer.from(JSON.stringify({ action: 'dialogmessage', msg: 'zippingFile', file: ((process.platform == 'win32') ? (name.split('/').join('\\')) : name), progress: p }))); }, 1000)); this.zip.pipe(out); break; + case 'unzip': + if (this.unzip != null) return; // Unzip operating is currently running, exit now. + this.unzip = require('zip-reader').read(cmd.input); + this.unzip._dest = cmd.dest; + this.unzip.xws = this; + this.unzip.then(function (zipped) { + this.xws.write(Buffer.from(JSON.stringify({ action: 'dialogmessage', msg: 'unzipping' }))); + zipped.xws = this.xws; + zipped.extractAll(this._dest).then(function () { // finished extracting + zipped.xws.write(Buffer.from(JSON.stringify({ action: 'dialogmessage', msg: null }))); + zipped.xws.write(Buffer.from(JSON.stringify({ action: 'refresh' }))); + delete zipped.xws.unzip; + }, function (e) { // error extracting + zipped.xws.write(Buffer.from(JSON.stringify({ action: 'dialogmessage', msg: 'unziperror', error: e }))); + delete zipped.xws.unzip; + }); + }, function (e) { this.xws.write(Buffer.from(JSON.stringify({ action: 'dialogmessage', msg: 'unziperror', error: e }))); delete this.xws.unzip }); + break; case 'cancel': // Cancel zip operation if present try { this.zipcancel = true; this.zip.cancel(function () { }); } catch (ex) { } @@ -3674,7 +3758,14 @@ function onTunnelControlData(data, ws) { { // Desktop // Switch the user input from websocket to webrtc at this point. 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.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc2\"}'); // Indicates we will no longer get any data on websocket, switching to WebRTC at this point. @@ -3733,6 +3824,64 @@ function consoleHttpResponse(response) { response.close = function () { sendConsoleText('httprequest.response.close', this.sessionid); consoleHttpRequest = null; } } +// Open a local file on current user's desktop +function openFileOnDesktop(file) { + var child = null; + try { + switch (process.platform) { + case 'win32': + var uid = require('user-sessions').consoleUid(); + var user = require('user-sessions').getUsername(uid); + var domain = require('user-sessions').getDomain(uid); + var task = { name: 'MeshChatTask', user: user, domain: domain, execPath: (require('fs').statSync(file).isDirectory() ? process.env['windir'] + '\\explorer.exe' : file) }; + if (require('fs').statSync(file).isDirectory()) task.arguments = [file]; + try { + require('win-tasks').addTask(task); + require('win-tasks').getTask({ name: 'MeshChatTask' }).run(); + require('win-tasks').deleteTask('MeshChatTask'); + return (true); + } + catch (ex) { + var taskoptions = { env: { _target: (require('fs').statSync(file).isDirectory() ? process.env['windir'] + '\\explorer.exe' : file), _user: '"' + domain + '\\' + user + '"' }, _args: "" }; + if (require('fs').statSync(file).isDirectory()) taskoptions.env._args = file; + for (var c1e in process.env) { + taskoptions.env[c1e] = process.env[c1e]; + } + var child = require('child_process').execFile(process.env['windir'] + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', ['powershell', '-noprofile', '-nologo', '-command', '-'], taskoptions); + child.stderr.on('data', function (c) { }); + child.stdout.on('data', function (c) { }); + child.stdin.write('SCHTASKS /CREATE /F /TN MeshChatTask /SC ONCE /ST 00:00 '); + if (user) { child.stdin.write('/RU $env:_user '); } + child.stdin.write('/TR "$env:_target $env:_args"\r\n'); + child.stdin.write('$ts = New-Object -ComObject Schedule.service\r\n'); + child.stdin.write('$ts.connect()\r\n'); + child.stdin.write('$tsfolder = $ts.getfolder("\\")\r\n'); + child.stdin.write('$task = $tsfolder.GetTask("MeshChatTask")\r\n'); + child.stdin.write('$taskdef = $task.Definition\r\n'); + child.stdin.write('$taskdef.Settings.StopIfGoingOnBatteries = $false\r\n'); + child.stdin.write('$taskdef.Settings.DisallowStartIfOnBatteries = $false\r\n'); + child.stdin.write('$taskdef.Actions.Item(1).Path = $env:_target\r\n'); + child.stdin.write('$taskdef.Actions.Item(1).Arguments = $env:_args\r\n'); + child.stdin.write('$tsfolder.RegisterTaskDefinition($task.Name, $taskdef, 4, $null, $null, $null)\r\n'); + child.stdin.write('SCHTASKS /RUN /TN MeshChatTask\r\n'); + child.stdin.write('SCHTASKS /DELETE /F /TN MeshChatTask\r\nexit\r\n'); + child.waitExit(); + } + break; + case 'linux': + child = require('child_process').execFile('/usr/bin/xdg-open', ['xdg-open', file], { uid: require('user-sessions').consoleUid() }); + break; + case 'darwin': + child = require('child_process').execFile('/usr/bin/open', ['open', file]); + break; + default: + // Unknown platform, ignore this command. + break; + } + } catch (ex) { } + return child; +} + // Open a web browser to a specified URL on current user's desktop function openUserDesktopUrl(url) { if ((url.toLowerCase().startsWith('http://') == false) && (url.toLowerCase().startsWith('https://') == false)) { return null; } @@ -3798,15 +3947,15 @@ function processConsoleCommand(cmd, args, rights, sessionid) { var response = null; switch (cmd) { case 'help': { // Displays available commands - var fin = '', f = '', availcommands = 'domain,translations,agentupdate,errorlog,msh,timerinfo,coreinfo,coredump,service,fdsnapshot,fdcount,startupoptions,alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,wslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,openurl,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,wallpaper,agentmsg,task,uninstallagent,display'; + var fin = '', f = '', availcommands = 'domain,translations,agentupdate,errorlog,msh,timerinfo,coreinfo,coreinfoupdate,coredump,service,fdsnapshot,fdcount,startupoptions,alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,wslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,openurl,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,wallpaper,agentmsg,task,uninstallagent,display,openfile'; if (require('os').dns != null) { availcommands += ',dnsinfo'; } try { require('linux-dhcp'); availcommands += ',dhcp'; } catch (ex) { } if (process.platform == 'win32') { - availcommands += ',cs,wpfhwacceleration,uac,volumes,rdpport'; + availcommands += ',bitlocker,cs,wpfhwacceleration,uac,volumes,rdpport,deskbackground'; if (bcdOK()) { availcommands += ',safemode'; } if (require('notifybar-desktop').DefaultPinned != null) { availcommands += ',privacybar'; } 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 (process.platform != 'freebsd') { availcommands += ',vm'; } @@ -3963,6 +4112,14 @@ function processConsoleCommand(cmd, args, rights, sessionid) { case 'volumes': response = JSON.stringify(require('win-volumes').getVolumes(), null, 1); break; + case 'bitlocker': + if (process.platform == 'win32') { + if (require('win-volumes').volumes_promise != null) { + var p = require('win-volumes').volumes_promise(); + p.then(function (res) { sendConsoleText(JSON.stringify(cleanGetBitLockerVolumeInfo(res), null, 1), this.session); }); + } + } + break; case 'dhcp': // This command is only supported on Linux, this is because Linux does not give us the DNS suffix for each network adapter independently so we have to ask the DHCP server. { try { require('linux-dhcp'); } catch (ex) { response = 'Unknown command "dhcp", type "help" for list of available commands.'; break; } @@ -4068,7 +4225,67 @@ function processConsoleCommand(cmd, args, rights, sessionid) { } break; case 'msh': - response = JSON.stringify(_MSH(), null, 2); + if (args['_'].length == 0) { + response = JSON.stringify(_MSH(), null, 2); + } else if (args['_'].length > 3) { + response = 'Proper usage: msh [get|set|delete]\r\nmsh get MeshServer\r\nmsh set abc "xyz"\r\nmsh delete abc'; + } else { + var mshFileName = process.execPath.replace('.exe','') + '.msh'; + switch (args['_'][0].toLocaleLowerCase()) { + case 'get': + if (typeof args['_'][1] != 'string' || args['_'].length > 2) { + response = 'Proper usage: msh get MeshServer'; + } else if(_MSH()[args['_'][1]]) { + response = _MSH()[args['_'][1]]; + } else { + response = "Unknown Value: " + args['_'][1]; + } + break; + case 'set': + if (typeof args['_'][1] != 'string' || typeof args['_'][2] != 'string') { + response = 'Proper usage: msh set abc "xyz"'; + } else { + var jsonToSave = _MSH(); + jsonToSave[args['_'][1]] = args['_'][2]; + var updatedContent = ''; + for (var key in jsonToSave) { + if (jsonToSave.hasOwnProperty(key)) { + updatedContent += key + '=' + jsonToSave[key] + '\n'; + } + } + try { + require('fs').writeFileSync(mshFileName, updatedContent); + response = "msh set " + args['_'][1] + " successful" + } catch (ex) { + response = "msh set " + args['_'][1] + " unsuccessful"; + } + } + break; + case 'delete': + if (typeof args['_'][1] != 'string') { + response = 'Proper usage: msh delete abc'; + } else { + var jsonToSave = _MSH(); + delete jsonToSave[args['_'][1]]; + var updatedContent = ''; + for (var key in jsonToSave) { + if (jsonToSave.hasOwnProperty(key)) { + updatedContent += key + '=' + jsonToSave[key] + '\n'; + } + } + try { + require('fs').writeFileSync(mshFileName, updatedContent); + response = "msh delete " + args['_'][1] + " successful" + } catch (ex) { + response = "msh delete " + args['_'][1] + " unsuccessful"; + } + } + break; + default: + response = 'Proper usage: msh [get|set|delete]\r\nmsh get MeshServer\r\nmsh set abc "xyz"\r\nmsh delete abc'; + break; + } + } break; case 'dnsinfo': if (require('os').dns == null) { @@ -4143,12 +4360,13 @@ function processConsoleCommand(cmd, args, rights, sessionid) { break; } case 'coreinfoupdate': { - sendPeriodicServerUpdate(); + sendPeriodicServerUpdate(null, true); + response = "Core Info Update Requested" break; } case 'agentmsg': { 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 { if ((args['_'][0] == 'add') && (args['_'].length > 1)) { var msgID, iconIndex = 0; @@ -4256,12 +4474,12 @@ function processConsoleCommand(cmd, args, rights, sessionid) { break; case 'unzip': if (args['_'].length == 0) { - response = "Proper usage: unzip input, destination"; // Display usage + response = "Proper usage: unzip input,destination"; // Display usage } else { var p = args['_'].join(' ').split(','); - if (p.length != 2) { response = "Proper usage: unzip input, destination"; break; } // Display usage - var prom = require('zip-reader').read(p[0]); - prom._dest = p[1]; + if (p.length != 2) { response = "Proper usage: unzip input,destination"; break; } // Display usage + var prom = require('zip-reader').read(p[0].trim()); + prom._dest = p[1].trim(); prom.self = this; prom.sessionid = sessionid; prom.then(function (zipped) { @@ -4366,10 +4584,11 @@ function processConsoleCommand(cmd, args, rights, sessionid) { if (process.platform == 'win32') { // Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value 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) { 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 { response = "Agent Size: " + actualSize + " kb"; } } else @@ -4631,6 +4850,11 @@ function processConsoleCommand(cmd, args, rights, sessionid) { else { if (openUserDesktopUrl(args['_'][0]) == null) { response = 'Failed.'; } else { response = 'Success.'; } } break; } + case 'openfile': { + if (args['_'].length != 1) { response = 'Proper usage: openfile (filepath)'; } // Display usage + else { if (openFileOnDesktop(args['_'][0]) == null) { response = 'Failed.'; } else { response = 'Success.'; } } + break; + } case 'users': { if (meshCoreObj.users == null) { response = 'Active users are unknown.'; } else { response = 'Active Users: ' + meshCoreObj.users.join(', ') + '.'; } require('user-sessions').enumerateUsers().then(function (u) { for (var i in u) { sendConsoleText(u[i]); } }); @@ -4742,8 +4966,11 @@ function processConsoleCommand(cmd, args, rights, sessionid) { } case 'sysinfo': { // Return system information getSystemInformation(function (results, err) { - if (results == null) { sendConsoleText(err, this.sessionid); } else { + if (results == null) { + sendConsoleText(err, this.sessionid); + } else { sendConsoleText(JSON.stringify(results, null, 1), this.sessionid); + mesh.SendCommand({ action: 'sysinfo', sessionid: this.sessionid, data: results }); } }); break; @@ -4756,6 +4983,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { response += '\r\nServer Connection: ' + mesh.isControlChannelConnected + ', State: ' + meshServerConnectionState + '.'; var oldNodeId = db.Get('OldNodeId'); if (oldNodeId != null) { response += '\r\nOldNodeID: ' + oldNodeId + '.'; } + response += '\r\nNode ID: ' + Buffer.from(require('_agentNodeId')(), 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); if (process.platform == 'linux' || process.platform == 'freebsd') { response += '\r\nX11 support: ' + require('monitor-info').kvm_x11_support + '.'; } response += '\r\nApplication Location: ' + process.cwd(); //response += '\r\Debug Console: ' + debugConsole + '.'; @@ -5169,6 +5397,13 @@ function processConsoleCommand(cmd, args, rights, sessionid) { } 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 response = "Unknown command \"" + cmd + "\", type \"help\" for list of available commands."; break; @@ -5433,12 +5668,12 @@ function windows_execve(name, agentfilename, sessionid) { sendAgentMessage('Self Update failed because msvcrt.dll is missing', 3); return; } - + var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true }); var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize); 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==' + - ' "' + process.execPath + '" & copy "' + process.cwd() + agentfilename + '.update.exe" "' + process.execPath + '" & wmic service "' + name + '" call startservice & erase "' + process.cwd() + agentfilename + '.update.exe"', { wide: true }); + 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 + '" & net start "' + name + '" & erase "' + process.cwd() + agentfilename + '.update.exe"', { wide: true }); arg1.pointerBuffer().copy(args.toBuffer()); arg2.pointerBuffer().copy(args.toBuffer(), require('_GenericMarshal').PointerSize); @@ -5628,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 meInfoStr = null; + LastPeriodicServerUpdate = null; sendPeriodicServerUpdate(null, true); if (selfInfoUpdateTimer == null) { selfInfoUpdateTimer = setInterval(sendPeriodicServerUpdate, 1200000); // 20 minutes @@ -5669,6 +5905,7 @@ function sendNetworkUpdate(force) { function sendPeriodicServerUpdate(flags, force) { if (meshServerConnectionState == 0) return; // Not connected to server, do nothing. if (!flags) { flags = 0xFFFFFFFF; } + if (!force) { force = false; } // If we have a connected MEI, get Intel ME information if ((flags & 1) && (amt != null) && (amt.state == 2)) { @@ -5701,6 +5938,7 @@ function sendPeriodicServerUpdate(flags, force) { }); } catch (ex) { } } + // Get Defender for Windows Server try { var d = require('win-info').defender(); @@ -5708,7 +5946,7 @@ function sendPeriodicServerUpdate(flags, force) { meshCoreObj.defender = res; meshCoreObjChanged(); }); - } catch (ex){ } + } catch (ex) { } } // Send available data right now @@ -5722,6 +5960,26 @@ function sendPeriodicServerUpdate(flags, force) { } } +// Sort the names in an object +function sortObject(obj) { return Object.keys(obj).sort().reduce(function(a, v) { a[v] = obj[v]; return a; }, {}); } + +// Fix the incoming data and cut down how much data we use +function cleanGetBitLockerVolumeInfo(volumes) { + for (var i in volumes) { + const v = volumes[i]; + if (typeof v.size == 'string') { v.size = parseInt(v.size); } + if (typeof v.sizeremaining == 'string') { v.sizeremaining = parseInt(v.sizeremaining); } + if (v.identifier == '') { delete v.identifier; } + if (v.name == '') { delete v.name; } + if (v.removable != true) { delete v.removable; } + if (v.cdrom != true) { delete v.cdrom; } + if (v.protectionStatus == 'On') { v.protectionStatus = true; } else { delete v.protectionStatus; } + if (v.volumeStatus == 'FullyDecrypted') { delete v.volumeStatus; } + if (v.recoveryPassword == '') { delete v.recoveryPassword; } + } + return sortObject(volumes); +} + // Once we are done collecting all the data, send to server if needed var LastPeriodicServerUpdate = null; var PeriodicServerUpdateNagleTimer = null; diff --git a/agents/modules_meshcmd/sysinfo.js b/agents/modules_meshcmd/sysinfo.js index 02c355d9..611a7b1a 100644 --- a/agents/modules_meshcmd/sysinfo.js +++ b/agents/modules_meshcmd/sysinfo.js @@ -225,19 +225,14 @@ function macos_memUtilization() function windows_thermals() { var ret = []; - child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', '/namespace:\\\\root\\wmi', 'PATH', 'MSAcpi_ThermalZoneTemperature', 'get', 'CurrentTemperature']); - 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.waitExit(); - - 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)); } + try { + ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); + if (ret[0]) { + for (var i = 0; i < ret.length; ++i) { + ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2); + } } - } + } catch (ex) { } return (ret); } @@ -285,16 +280,10 @@ function macos_thermals() return (ret); } -switch(process.platform) -{ - case 'linux': - module.exports = { cpuUtilization: linux_cpuUtilization, memUtilization: linux_memUtilization, thermals: linux_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; -} +const platformConfig = { + linux: { cpuUtilization: linux_cpuUtilization, memUtilization: linux_memUtilization, thermals: linux_thermals }, + win32: { cpuUtilization: windows_cpuUtilization, memUtilization: windows_memUtilization, thermals: windows_thermals }, + darwin: { cpuUtilization: macos_cpuUtilization, memUtilization: macos_memUtilization, thermals: macos_thermals } +}; +module.exports = platformConfig[process.platform]; diff --git a/agents/modules_meshcore/computer-identifiers.js b/agents/modules_meshcore/computer-identifiers.js index 0223e211..ce5e3520 100644 --- a/agents/modules_meshcore/computer-identifiers.js +++ b/agents/modules_meshcore/computer-identifiers.js @@ -70,31 +70,38 @@ function linux_identifiers() var values = {}; if (!require('fs').existsSync('/sys/class/dmi/id')) { - if(require('fs').existsSync('/sys/firmware/devicetree/base/model')){ - if(require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim().startsWith('Raspberry')){ + if (require('fs').existsSync('/sys/firmware/devicetree/base/model')) { + if (require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim().startsWith('Raspberry')) { identifiers['board_vendor'] = 'Raspberry Pi'; 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(); - }else{ + 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 { throw('Unknown board'); } - }else { + } else { throw ('this platform does not have DMI statistics'); } } else { var entries = require('fs').readdirSync('/sys/class/dmi/id'); - for(var i in entries) - { - if (require('fs').statSync('/sys/class/dmi/id/' + entries[i]).isFile()) - { - try - { + for (var i in entries) { + if (require('fs').statSync('/sys/class/dmi/id/' + entries[i]).isFile()) { + try { ret[entries[i]] = require('fs').readFileSync('/sys/class/dmi/id/' + entries[i]).toString().trim(); - } - catch(z) - { - } - if (ret[entries[i]] == 'None') { delete ret[entries[i]];} + } catch(z) { } + if (ret[entries[i]] == 'None') { delete ret[entries[i]]; } } } entries = null; @@ -144,11 +151,19 @@ function linux_identifiers() child.stdin.write("lshw -class disk | tr '\\n' '`' | awk '" + '{ len=split($0,lines,"*"); printf "["; for(i=2;i<=len;++i) { model=""; caption=""; size=""; clen=split(lines[i],item,"`"); for(j=2;j1 {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(); + try { ret.volumes = JSON.parse(child.stdout.str.trim()); } catch (xx) { } + child = null; values.identifiers = identifiers; values.linux = ret; trimIdentifiers(values.identifiers); - child = null; var dmidecode = require('lib-finder').findBinary('dmidecode'); if (dmidecode != null) @@ -343,6 +358,36 @@ function linux_identifiers() child = null; } + // Linux Last Boot Up Time + try { + child = require('child_process').execFile('/usr/bin/uptime', ['', '-s']); // must include blank value at begining for some reason? + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stderr.on('data', function () { }); + child.waitExit(); + 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; + } catch (ex) { } + + // Linux TPM + try { + if (require('fs').statSync('/sys/class/tpm/tpm0').isDirectory()){ + values.tpm = { + SpecVersion: require('fs').readFileSync('/sys/class/tpm/tpm0/tpm_version_major').toString().trim() + } + } + } catch (ex) { } + return (values); } @@ -377,94 +422,6 @@ function windows_wmic_results(str) 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,DriveType | ConvertTo-Csv -NoTypeInformation\nexit\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], - removable: tokens[4].split('"')[1] == 'Removable' - }; - } - } - 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\nexit\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 str = ''; - var foundMarkedLine = false; - var password = ''; - var child = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'manage-bde -protectors -get ', tokens[0].split('"')[1], ' -Type recoverypassword'], {}); - child.stdout.on('data', function (chunk) { str += chunk.toString(); }); - child.stderr.on('data', function (chunk) { str += chunk.toString(); }); - child.waitExit(); - var lines = str.split(/\r?\n/); - for (var i = 0; i < lines.length; i++) { - if (lines[i].trim() !== '' && lines[i].includes('Password:') && !lines[i].includes('Numerical Password:')) { - if (i + 1 < lines.length && lines[i + 1].trim() !== '') { - password = lines[i + 1].trim(); - foundMarkedLine = true; - } - if (foundMarkedLine) break; - } - } - ret[key].recoveryPassword = (foundMarkedLine ? password : ''); - } catch(ex) { } - } - } - this.promise._res(ret); - }); - }); - return (p2); -} - function windows_identifiers() { var ret = { windows: {} }; @@ -549,11 +506,28 @@ function windows_identifiers() } try { ret.identifiers.cpu_name = ret.windows.cpu[0].Name; } catch (x) { } + + // Windows TPM + IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }; + try { + values = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftTpm', "SELECT * FROM Win32_Tpm", ['IsActivated_InitialValue','IsEnabled_InitialValue','IsOwned_InitialValue','ManufacturerId','ManufacturerVersion','SpecVersion']); + if(values[0]) { + ret.tpm = { + SpecVersion: values[0].SpecVersion.split(",")[0], + ManufacturerId: IntToStr(values[0].ManufacturerId).replace(/[^\x00-\x7F]/g, ""), + ManufacturerVersion: values[0].ManufacturerVersion, + IsActivated: values[0].IsActivated_InitialValue, + IsEnabled: values[0].IsEnabled_InitialValue, + IsOwned: values[0].IsOwned_InitialValue, + } + } + } catch (ex) { } + return (ret); } function macos_identifiers() { - var ret = { identifiers: {} }; + var ret = { identifiers: {}, darwin: {} }; var child; child = require('child_process').execFile('/bin/sh', ['sh']); @@ -592,42 +566,171 @@ function macos_identifiers() child.waitExit(); ret.identifiers.cpu_name = child.stdout.str.trim(); + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stdin.write('system_profiler SPMemoryDataType\nexit\n'); + child.waitExit(); + var lines = child.stdout.str.trim().split('\n'); + if(lines.length > 0) { + const memorySlots = []; + if(lines[2].trim().includes('Memory Slots:')) { // OLD MACS WITH SLOTS + var memorySlots1 = child.stdout.str.split(/\n{2,}/).slice(3); + memorySlots1.forEach(function(slot,index) { + var lines = slot.split('\n'); + if(lines.length == 1){ // start here + if(lines[0].trim()!=''){ + var slotObj = { DeviceLocator: lines[0].trim().replace(/:$/, '') }; // Initialize name as an empty string + var nextline = memorySlots1[index+1].split('\n'); + nextline.forEach(function(line) { + if (line.trim() !== '') { + var parts = line.split(':'); + var key = parts[0].trim(); + var value = parts[1].trim(); + value = (key == 'Part Number' || key == 'Manufacturer') ? hexToAscii(parts[1].trim()) : parts[1].trim(); + slotObj[key.replace(' ','')] = value; // Store attribute in the slot object + } + }); + memorySlots.push(slotObj); + } + } + }); + } else { // NEW MACS WITHOUT SLOTS + memorySlots.push({ DeviceLocator: "Onboard Memory", Size: lines[2].split(":")[1].trim(), PartNumber: lines[3].split(":")[1].trim(), Manufacturer: lines[4].split(":")[1].trim() }) + } + ret.darwin.memory = memorySlots; + } + + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stdin.write('diskutil info -all\nexit\n'); + child.waitExit(); + var sections = child.stdout.str.split('**********\n'); + if(sections.length > 0){ + var devices = []; + for (var i = 0; i < sections.length; i++) { + var lines = sections[i].split('\n'); + var deviceInfo = {}; + var wholeYes = false; + var physicalYes = false; + var oldmac = false; + for (var j = 0; j < lines.length; j++) { + var keyValue = lines[j].split(':'); + var key = keyValue[0].trim(); + var value = keyValue[1] ? keyValue[1].trim() : ''; + if (key === 'Virtual') oldmac = true; + if (key === 'Whole' && value === 'Yes') wholeYes = true; + if (key === 'Virtual' && value === 'No') physicalYes = true; + if(value && key === 'Device / Media Name'){ + deviceInfo['Caption'] = value; + } + if(value && key === 'Disk Size'){ + deviceInfo['Size'] = value.split(' ')[0] + ' ' + value.split(' ')[1]; + } + } + if (wholeYes) { + if (oldmac) { + if (physicalYes) devices.push(deviceInfo); + } else { + devices.push(deviceInfo); + } + } + } + ret.identifiers.storage_devices = devices; + } + + // Fetch storage volumes using df + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('df -aHY | awk \'NR>1 {printf "{\\"size\\":\\"%s\\",\\"used\\":\\"%s\\",\\"available\\":\\"%s\\",\\"mount_point\\":\\"%s\\",\\"type\\":\\"%s\\"},", $3, $4, $5, $10, $2}\' | sed \'$ s/,$//\' | awk \'BEGIN {printf "["} {printf "%s", $0} END {printf "]"}\'\nexit\n'); + child.waitExit(); + try { + ret.darwin.volumes = JSON.parse(child.stdout.str.trim()); + for (var index = 0; index < ret.darwin.volumes.length; index++) { + if (ret.darwin.volumes[index].type == 'auto_home'){ + ret.darwin.volumes.splice(index,1); + } + } + if (ret.darwin.volumes.length == 0) { // not sonima OS so dont show type for now + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('df -aH | awk \'NR>1 {printf "{\\"size\\":\\"%s\\",\\"used\\":\\"%s\\",\\"available\\":\\"%s\\",\\"mount_point\\":\\"%s\\"},", $2, $3, $4, $9}\' | sed \'$ s/,$//\' | awk \'BEGIN {printf "["} {printf "%s", $0} END {printf "]"}\'\nexit\n'); + child.waitExit(); + try { + ret.darwin.volumes = JSON.parse(child.stdout.str.trim()); + for (var index = 0; index < ret.darwin.volumes.length; index++) { + if (ret.darwin.volumes[index].size == 'auto_home'){ + ret.darwin.volumes.splice(index,1); + } + } + } catch (xx) { } + } + } catch (xx) { } + child = null; + + // MacOS Last Boot Up Time + try { + child = require('child_process').execFile('/usr/sbin/sysctl', ['', 'kern.boottime']); // must include blank value at begining for some reason? + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stderr.on('data', function () { }); + child.waitExit(); + const timestampMatch = /\{ sec = (\d+), usec = \d+ \}/.exec(child.stdout.str.trim()); + if (!ret.darwin) { + ret.darwin = { LastBootUpTime: parseInt(timestampMatch[1]) }; + } else { + ret.darwin.LastBootUpTime = parseInt(timestampMatch[1]); + } + child = null; + } catch (ex) { } trimIdentifiers(ret.identifiers); - child = null; return (ret); } +function hexToAscii(hexString) { + if(!hexString.startsWith('0x')) return hexString.trim(); + hexString = hexString.startsWith('0x') ? hexString.slice(2) : hexString; + var str = ''; + for (var i = 0; i < hexString.length; i += 2) { + var hexPair = hexString.substr(i, 2); + str += String.fromCharCode(parseInt(hexPair, 16)); + } + str = str.replace(/[\u007F-\uFFFF]/g, ''); // Remove characters from 0x0080 to 0xFFFF + return str.trim(); +} + 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.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(); - - try - { - var tok = child.stdout.str.split('{')[1].split('}')[0]; - var val = tok.split(',')[0]; - return (parseInt(val)); - } - catch (e) - { + try { + return (parseInt(child.stdout.str)); + } catch (e) { return (2); // unknown } } function win_systemType() { - var CSV = '/FORMAT:"' + require('util-language').wmicXslPath + 'csv"'; - var child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', 'ComputerSystem', 'get', 'PCSystemType', CSV]); - 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.waitExit(); + try { + var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']); + if (tokens[0]) { + return (parseInt(tokens[0]['PCSystemType'])); + } 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) @@ -751,6 +854,8 @@ module.exports.isVM = function isVM() case 'VMware, Inc.': case 'Xen': case 'SeaBIOS': + case 'EFI Development Kit II / OVMF': + case 'Proxmox distribution of EDK II': ret = true; break; default: @@ -788,15 +893,10 @@ module.exports.isVM = function isVM() return (ret); }; -if (process.platform == 'win32') -{ - module.exports.volumes_promise = windows_volumes; -} - // bios_date = BIOS->ReleaseDate // bios_vendor = BIOS->Manufacturer // bios_version = BIOS->SMBIOSBIOSVersion // board_name = BASEBOARD->Product = ioreg/board-id // board_serial = BASEBOARD->SerialNumber = ioreg/serial-number | ioreg/IOPlatformSerialNumber // board_vendor = BASEBOARD->Manufacturer = ioreg/manufacturer -// board_version = BASEBOARD->Version \ No newline at end of file +// board_version = BASEBOARD->Version diff --git a/agents/modules_meshcore/coretranslations.json b/agents/modules_meshcore/coretranslations.json index 4493ea3a..567a08f8 100644 --- a/agents/modules_meshcore/coretranslations.json +++ b/agents/modules_meshcore/coretranslations.json @@ -262,5 +262,29 @@ "desktopNotify": "{0} távoli asztali munkamenetet indított.", "fileNotify": "{0} távoli fájlmunkamenetet indított.", "privacyBar": "Asztal megosztás aktív: {0} felhasználóval" + }, + "ca": { + "allow": "Permetre", + "deny": "Negar", + "autoAllowForFive": "Accepta automàticament totes les connexions durant els propers 5 minuts", + "terminalConsent": "{0} sol·licitant accés al terminal remot. Accés garantit?", + "desktopConsent": "{0} sol·licitant accés a l'escriptori remot. Accés garantit?", + "fileConsent": "{0} sol·licitant accés remot al fitxer. Accés garantit?", + "terminalNotify": "{0} va iniciar una sessió de terminal remota.", + "desktopNotify": "{0} va iniciar una sessió d'escriptori remot.", + "fileNotify": "{0} va iniciar una sessió de fitxer remota.", + "privacyBar": "Compartint escriptori amb: {0}" + }, + "uk": { + "allow": "Дозволити", + "deny": "Відмовити", + "autoAllowForFive": "Автоматично приймати всі підключення впродовж наступних 5 хвилин", + "terminalConsent": "{0} запитує доступ до віддаленого терміналу. Надати доступ?", + "desktopConsent": "{0} запитує віддалений доступ до стільниці. Надати доступ?", + "fileConsent": "{0} запитує віддалений доступ до файлу. Надати доступ?", + "terminalNotify": "{0} почав сеанс віддаленого терміналу.", + "desktopNotify": "{0} розпочав сеанс віддаленої стільниці.", + "fileNotify": "{0} розпочав віддалений файловий сеанс.", + "privacyBar": "Поширити доступ до стільниці з: {0}" } } \ No newline at end of file diff --git a/agents/modules_meshcore/sysinfo.js b/agents/modules_meshcore/sysinfo.js index 78979f33..cc13574d 100644 --- a/agents/modules_meshcore/sysinfo.js +++ b/agents/modules_meshcore/sysinfo.js @@ -209,9 +209,13 @@ function macos_memUtilization() { var usage = lines[0].split(':')[1]; var bdown = usage.split(','); - - mem.MemTotal = parseInt(bdown[0].trim().split(' ')[0]); - mem.MemFree = parseInt(bdown[1].trim().split(' ')[0]); + if (bdown.length > 2){ // new style - PhysMem: 5750M used (1130M wired, 634M compressor), 1918M unused. + mem.MemFree = parseInt(bdown[2].trim().split(' ')[0]); + } else { // old style - PhysMem: 6683M used (1606M wired), 9699M unused. + mem.MemFree = parseInt(bdown[1].trim().split(' ')[0]); + } + mem.MemUsed = parseInt(bdown[0].trim().split(' ')[0]); + mem.MemTotal = (mem.MemFree + mem.MemUsed); mem.percentFree = ((mem.MemFree / mem.MemTotal) * 100);//.toFixed(2); mem.percentConsumed = (((mem.MemTotal - mem.MemFree) / mem.MemTotal) * 100);//.toFixed(2); return (mem); @@ -225,25 +229,14 @@ function macos_memUtilization() function windows_thermals() { 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']); - 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.waitExit(); - if(child.stdout.str.trim()!='') - { - 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]; } + try { + ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); + if (ret[0]) { + for (var i = 0; i < ret.length; ++i) { + ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2); } - ret.push(obj); } - } + } catch (ex) { } return (ret); } @@ -305,9 +298,14 @@ function macos_thermals() } } }); - child.stderr.str = ''; child.stderr.on('data', function (c) { this.str += c.toString(); }); - child.stdin.write('powermetrics -s smc\n'); - child.waitExit(5000); + child.stderr.on('data', function (c) { + if (c.toString().split('unable to get smc values').length > 1) { // error getting sensors so just kill + this.parent.kill(); + return; + } + }); + child.stdin.write('powermetrics -s smc -i 500 -n 1\n'); + child.waitExit(2000); } return (ret); } diff --git a/agents/modules_meshcore/win-info.js b/agents/modules_meshcore/win-info.js index e541520b..dd60eb12 100644 --- a/agents/modules_meshcore/win-info.js +++ b/agents/modules_meshcore/win-info.js @@ -18,28 +18,21 @@ var promise = require('promise'); function qfe() { - var child = require('child_process').execFile(process.env['windir'] + '\\System32\\wbem\\wmic.exe', ['wmic', 'qfe', 'list', 'full', '/FORMAT:CSV']); - 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.waitExit(); - - var lines = child.stdout.str.trim().split('\r\n'); - var keys = lines[0].split(','); - var i, key; - var tokens; - var result = []; - - 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]; } + try { + var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering'); + if (tokens[0]){ + for (var index = 0; index < tokens.length; index++) { + for (var key in tokens[index]) { + if (key.startsWith('__')) delete tokens[index][key]; + } + } + return (tokens); + } else { + return ([]); } - result.push(obj); + } catch (ex) { + return ([]); } - return (result); } function av() { @@ -228,6 +221,14 @@ function installedApps() catch(e)\ {\ }\ + try\ + {\ + val.installdate = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'InstallDate');\ + if (val.installdate == '') { delete val.installdate; }\ + }\ + catch(e)\ + {\ + }\ result.push(val);\ }\ console.log(JSON.stringify(result,'', 1));process.exit();"; @@ -250,8 +251,12 @@ function defender(){ ret.child.stdin.write('exit\r\n'); ret.child.on('exit', function (c) { if (this.stdout.str == '') { this.promise._resolve({}); return; } - var abc = JSON.parse(this.stdout.str.trim()) - this.promise._resolve({ RealTimeProtection: abc.RealTimeProtectionEnabled, TamperProtected: abc.IsTamperProtected }); + try { + 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); } diff --git a/agents/modules_meshcore/win-volumes.js b/agents/modules_meshcore/win-volumes.js index e0e11276..59e87c1a 100644 --- a/agents/modules_meshcore/win-volumes.js +++ b/agents/modules_meshcore/win-volumes.js @@ -39,17 +39,90 @@ function getVolumes() { ret[v[i].DeviceID] = trimObject(v[i]); } - - v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume'); - for (i in v) - { - var tmp = trimObject(v[i]); - for (var k in tmp) + try { + v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume'); + for (i in v) { - 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); } -module.exports = { getVolumes: function () { try { return (getVolumes()); } catch (x) { return ({}); } } }; \ No newline at end of file +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 +}; \ No newline at end of file diff --git a/agents/recoverycore.js b/agents/recoverycore.js index 169b170c..5d9f13bc 100644 --- a/agents/recoverycore.js +++ b/agents/recoverycore.js @@ -485,8 +485,8 @@ function windows_execve(name, agentfilename, sessionid) { var cmd = require('_GenericMarshal').CreateVariable(process.env['windir'] + '\\system32\\cmd.exe', { wide: true }); var args = require('_GenericMarshal').CreateVariable(3 * require('_GenericMarshal').PointerSize); 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==' + - ' "' + process.execPath + '" & copy "' + cwd + agentfilename + '.update.exe" "' + process.execPath + '" & wmic service "' + name + '" call startservice & erase "' + cwd + agentfilename + '.update.exe"', { wide: true }); + var arg2 = require('_GenericMarshal').CreateVariable('/C net stop "' + name + '" & "' + cwd + agentfilename + '.update.exe" -b64exec ' + 'dHJ5CnsKICAgIHZhciBzZXJ2aWNlTG9jYXRpb24gPSBwcm9jZXNzLmFyZ3YucG9wKCkudG9Mb3dlckNhc2UoKTsKICAgIHJlcXVpcmUoJ3Byb2Nlc3MtbWFuYWdlcicpLmVudW1lcmF0ZVByb2Nlc3NlcygpLnRoZW4oZnVuY3Rpb24gKHByb2MpCiAgICB7CiAgICAgICAgZm9yICh2YXIgcCBpbiBwcm9jKQogICAgICAgIHsKICAgICAgICAgICAgaWYgKHByb2NbcF0ucGF0aCAmJiAocHJvY1twXS5wYXRoLnRvTG93ZXJDYXNlKCkgPT0gc2VydmljZUxvY2F0aW9uKSkKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvY2Vzcy5raWxsKHByb2NbcF0ucGlkKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwcm9jZXNzLmV4aXQoKTsKICAgIH0pOwp9CmNhdGNoIChlKQp7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQ==' + + ' "' + process.execPath + '" & copy "' + cwd + agentfilename + '.update.exe" "' + process.execPath + '" & net start "' + name + '" & erase "' + cwd + agentfilename + '.update.exe"', { wide: true }); if (name == null) { diff --git a/amtmanager.js b/amtmanager.js index 4c1d6026..10e19647 100644 --- a/amtmanager.js +++ b/amtmanager.js @@ -707,7 +707,15 @@ module.exports.CreateAmtManager = function (parent) { dev.aquired.controlMode = responses['IPS_HostBasedSetupService'].response.CurrentControlMode; // 1 = CCM, 2 = ACM if (typeof stack.wsman.comm.amtVersion == 'string') { // Set the Intel AMT version using the HTTP header if present var verSplit = stack.wsman.comm.amtVersion.split('.'); - if (verSplit.length >= 3) { dev.aquired.version = verSplit[0] + '.' + verSplit[1] + '.' + verSplit[2]; dev.aquired.majorver = parseInt(verSplit[0]); dev.aquired.minorver = parseInt(verSplit[1]); } + if (verSplit.length >= 2) { + dev.aquired.version = verSplit[0] + '.' + verSplit[1]; + dev.aquired.majorver = parseInt(verSplit[0]); + dev.aquired.minorver = parseInt(verSplit[1]); + if (verSplit.length >= 3) { + dev.aquired.version = verSplit[0] + '.' + verSplit[1] + '.' + verSplit[2]; + dev.aquired.maintenancever = parseInt(verSplit[2]); + } + } } dev.aquired.realm = stack.wsman.comm.digestRealm; dev.aquired.user = dev.intelamt.user = stack.wsman.comm.user; @@ -947,7 +955,7 @@ module.exports.CreateAmtManager = function (parent) { }); } - // Perform a power action: 2 = Power up, 5 = Power cycle, 8 = Power down, 10 = Reset, 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL + // Perform a power action: 2 = Power up, 5 = Power cycle, 8 = Power down, 10 = Reset, 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL, 15 = Power on to PXE, 16 = Reset to PXE function performPowerAction(nodeid, action) { console.log('performPowerAction', nodeid, action); var devices = obj.amtDevices[nodeid]; @@ -962,7 +970,7 @@ module.exports.CreateAmtManager = function (parent) { // Action: 2 = Power up, 5 = Power cycle, 8 = Power down, 10 = Reset try { dev.amtstack.RequestPowerStateChange(action, performPowerActionResponse); } catch (ex) { } } else { - // 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL + // 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL, 15 = Power on to PXE, 16 = Reset to PXE dev.amtstack.BatchEnum(null, ['*AMT_BootSettingData'], performAdvancedPowerActionResponse); } } @@ -995,8 +1003,8 @@ module.exports.CreateAmtManager = function (parent) { // Ready boot parameters bootSettingData['BIOSSetup'] = ((action >= 11) && (action <= 14)); bootSettingData['UseSOL'] = ((action >= 13) && (action <= 14)); - if ((action == 11) || (action == 13)) { dev.powerAction = 2; } // Power on - if ((action == 12) || (action == 14)) { dev.powerAction = 10; } // Reset + if ((action == 11) || (action == 13) || (action == 15)) { dev.powerAction = 2; } // Power on + if ((action == 12) || (action == 14) || (action == 16)) { dev.powerAction = 10; } // Reset // Set boot parameters dev.amtstack.Put('AMT_BootSettingData', bootSettingData, function (stack, name, response, status, tag) { @@ -1007,7 +1015,8 @@ module.exports.CreateAmtManager = function (parent) { const dev = stack.dev; if ((obj.amtDevices[dev.nodeid] == null) || (status != 200)) return; // Device no longer exists or error // Set boot order - dev.amtstack.CIM_BootConfigSetting_ChangeBootOrder(null, function (stack, name, response, status) { + var bootDevice = (action === 15 || action === 16) ? '
http://schemas.xmlsoap.org/ws/2004/08/addressing
http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootSourceSettingIntel(r) AMT: Force PXE Boot' : null; + dev.amtstack.CIM_BootConfigSetting_ChangeBootOrder(bootDevice, function (stack, name, response, status) { const dev = stack.dev; if ((obj.amtDevices[dev.nodeid] == null) || (status != 200)) return; // Device no longer exists or error // Perform power action @@ -1060,7 +1069,7 @@ module.exports.CreateAmtManager = function (parent) { if (status != 200) { dev.consoleMsg("Failed to get security information (" + status + ")."); delete dev.ocrfile; return; } // Check if this Intel AMT device supports OCR - if (responses['AMT_PublicKeyCertificate'].responses['ForceUEFIHTTPSBoot'] !== true) { + if (responses['AMT_BootCapabilities'].response['ForceUEFIHTTPSBoot'] !== true) { dev.consoleMsg("This Intel AMT device does not support UEFI HTTPS boot (" + status + ")."); delete dev.ocrfile; return; } @@ -1090,11 +1099,14 @@ module.exports.CreateAmtManager = function (parent) { // Generate the one-time URL. var cookie = obj.parent.encodeCookie({ a: 'f', f: dev.ocrfile }, obj.parent.loginCookieEncryptionKey) - var url = 'https://' + parent.webserver.certificates.AmtMpsName + ':' + ((parent.args.mpsaliasport != null) ? parent.args.mpsaliasport : parent.args.mpsport) + '/c/' + cookie + '.iso'; + var url = 'https://' + parent.webserver.certificates.AmtMpsName + ':' + ((parent.args.mpsaliasport != null) ? parent.args.mpsaliasport : parent.args.mpsport) + '/c/' + cookie + '.efi'; delete dev.ocrfile; // Generate the boot data for OCR with URL var r = response.Body; + r['BIOSPause'] = false; + r['BIOSSetup'] = false; + r['EnforceSecureBoot'] = false; r['UefiBootParametersArray'] = Buffer.from(makeUefiBootParam(1, url) + makeUefiBootParam(20, 1, 1) + makeUefiBootParam(30, 0, 2), 'binary').toString('base64'); r['UefiBootNumberOfParams'] = 3; r['BootMediaIndex'] = 0; // Do not use boot media index for One Click Recovery (OCR) @@ -1115,8 +1127,7 @@ module.exports.CreateAmtManager = function (parent) { dev.amtstack.SetBootConfigRole(1, function (stack, name, response, status) { if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request. if (status != 200) { dev.consoleMsg("Failed to set boot config role (" + status + ")."); return; } - var bootSource = 'Force OCR UEFI HTTPS Boot'; - dev.amtstack.CIM_BootConfigSetting_ChangeBootOrder((bootSource == null) ? bootSource : '
http://schemas.xmlsoap.org/ws/2004/08/addressing
http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootSourceSettingIntel(r) AMT: ' + bootSource + '', function (stack, name, response, status) { + dev.amtstack.CIM_BootConfigSetting_ChangeBootOrder('
http://schemas.xmlsoap.org/ws/2004/08/addressing
http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootSourceSettingIntel(r) AMT: Force OCR UEFI HTTPS Boot', function (stack, name, response, status) { if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request. if (status != 200) { dev.consoleMsg("Failed to set boot config (" + status + ")."); return; } dev.amtstack.RequestPowerStateChange(10, function (stack, name, response, status) { // 10 = Reset, 2 = Power Up @@ -2621,7 +2632,14 @@ module.exports.CreateAmtManager = function (parent) { if (domain && domain.amtmanager && (domain.amtmanager.tlsacmactivation == true)) { TlsAcmActivation = true; } // Check Intel AMT version - if (typeof dev.intelamt.ver == 'string') { var verSplit = dev.intelamt.ver.split('.'); if (verSplit.length >= 3) { dev.aquired.majorver = parseInt(verSplit[0]); dev.aquired.minorver = parseInt(verSplit[1]); } } + if (typeof dev.intelamt.ver == 'string') { + var verSplit = dev.intelamt.ver.split('.'); + if (verSplit.length >= 2) { + dev.aquired.majorver = parseInt(verSplit[0]); + dev.aquired.minorver = parseInt(verSplit[1]); + if (verSplit.length >= 3) { dev.aquired.maintenancever = parseInt(verSplit[2]); } + } + } // If this is Intel AMT 14 or better and allowed, we are going to attempt a host-based end-to-end TLS activation. if (TlsAcmActivation && (dev.aquired.majorver >= 14)) { @@ -2677,7 +2695,15 @@ module.exports.CreateAmtManager = function (parent) { dev.aquired.controlMode = 1; // 1 = CCM, 2 = ACM if (typeof dev.amtstack.wsman.comm.amtVersion == 'string') { var verSplit = dev.amtstack.wsman.comm.amtVersion.split('.'); - if (verSplit.length >= 3) { dev.aquired.version = verSplit[0] + '.' + verSplit[1] + '.' + verSplit[2]; dev.aquired.majorver = parseInt(verSplit[0]); dev.aquired.minorver = parseInt(verSplit[1]); } + if (verSplit.length >= 2) { + dev.aquired.version = verSplit[0] + '.' + verSplit[1]; + dev.aquired.majorver = parseInt(verSplit[0]); + dev.aquired.minorver = parseInt(verSplit[1]); + if (verSplit.length >= 3) { + dev.aquired.version = verSplit[0] + '.' + verSplit[1] + '.' + verSplit[2]; + dev.aquired.maintenancever = parseInt(verSplit[2]); + } + } } if ((typeof dev.mpsConnection.tag.meiState.OsHostname == 'string') && (typeof dev.mpsConnection.tag.meiState.OsDnsSuffix == 'string')) { dev.aquired.host = dev.mpsConnection.tag.meiState.OsHostname + '.' + dev.mpsConnection.tag.meiState.OsDnsSuffix; @@ -2812,8 +2838,10 @@ module.exports.CreateAmtManager = function (parent) { var vs = getInstance(amtlogicalelements, 'AMT')['VersionString']; if (vs != null) { dev.aquired.version = vs; - dev.aquired.versionmajor = parseInt(dev.aquired.version.split('.')[0]); - dev.aquired.versionminor = parseInt(dev.aquired.version.split('.')[1]); + version = dev.aquired.version.split('.') + dev.aquired.versionmajor = parseInt(version[0]); + dev.aquired.versionminor = parseInt(version[1]); + if (version.length > 2) { dev.aquired.versionmaintenance = parseInt(version[2]); } } } } @@ -2821,10 +2849,14 @@ module.exports.CreateAmtManager = function (parent) { // Fetch the Intel AMT version from HTTP stack if ((dev.amtversionstr == null) && (stack.wsman.comm.amtVersion != null)) { var s = stack.wsman.comm.amtVersion.split('.'); - if (s.length >= 3) { - dev.aquired.version = s[0] + '.' + s[1] + '.' + s[2]; + if (s.length >= 2) { + dev.aquired.version = s[0] + '.' + s[1] + '.'; dev.aquired.versionmajor = parseInt(s[0]); dev.aquired.versionminor = parseInt(s[1]); + if (s.length >= 3) { + dev.aquired.version = s[0] + '.' + s[1] + '.' + s[2]; + dev.aquired.versionmaintenance = parseInt(s[2]); + } } } diff --git a/amtprovisioningserver.js b/amtprovisioningserver.js index 84a07df9..d31b7b83 100644 --- a/amtprovisioningserver.js +++ b/amtprovisioningserver.js @@ -201,8 +201,10 @@ module.exports.CreateAmtProvisioningServer = function (parent, config) { var vs = getInstance(amtlogicalelements, 'AMT')['VersionString']; if (vs != null) { dev.aquired.version = vs; - dev.aquired.versionmajor = parseInt(dev.aquired.version.split('.')[0]); - dev.aquired.versionminor = parseInt(dev.aquired.version.split('.')[1]); + const versionSplit = parseInt(dev.aquired.version.split('.')); + dev.aquired.versionmajor = parseInt(versionSplit[0]); + dev.aquired.versionminor = parseInt(versionSplit[1]); + if (versionSplit.length >= 3) { dev.aquired.versionmaintenance = parseInt(versionSplit[2]); } } } } @@ -210,10 +212,14 @@ module.exports.CreateAmtProvisioningServer = function (parent, config) { // Fetch the Intel AMT version from HTTP stack if ((dev.amtversionstr == null) && (stack.wsman.comm.amtVersion != null)) { var s = stack.wsman.comm.amtVersion.split('.'); - if (s.length >= 3) { - dev.aquired.version = s[0] + '.' + s[1] + '.' + s[2]; + if (s.length >= 2) { + dev.aquired.version = s[0] + '.' + s[1]; dev.aquired.versionmajor = parseInt(s[0]); dev.aquired.versionminor = parseInt(s[1]); + if (s.length >= 3) { + dev.aquired.version = s[0] + '.' + s[1] + '.' + s[2]; + dev.aquired.versionmaintenance = parseInt(s[2]); + } } } diff --git a/apprelays.js b/apprelays.js index 966165f7..cd6bc5fa 100644 --- a/apprelays.js +++ b/apprelays.js @@ -1,4 +1,4 @@ -/** +/** * @description MeshCentral MSTSC & SSH relay * @author Ylian Saint-Hilaire & Bryan Roe * @copyright Intel Corporation 2018-2022 @@ -717,9 +717,10 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) { } } } - else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, 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('Cache-Control', 'no-store'); // Tell the browser not to cache the responses since since the relay port can be used for many relays } @@ -842,22 +843,21 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { obj.relaySocket.resume(); } } else { - if (typeof data == 'string') { - // Forward any ping/pong commands to the browser - var cmd = null; - try { cmd = JSON.parse(data); } catch (ex) { } + try { // Forward any ping/pong commands to the browser + var cmd = JSON.parse(data); if ((cmd != null) && (cmd.ctrlChannel == '102938')) { if (cmd.type == 'ping') { send(['ping']); } else if (cmd.type == 'pong') { send(['pong']); } } return; + } catch (ex) { // You are not JSON data so just send over relaySocket + obj.wsClient._socket.pause(); + try { + obj.relaySocket.write(data, function () { + if (obj.wsClient && obj.wsClient._socket) { try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); } } + }); + } catch (ex) { console.log(ex); obj.close(); } } - obj.wsClient._socket.pause(); - try { - obj.relaySocket.write(data, function () { - if (obj.wsClient && obj.wsClient._socket) { try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); } } - }); - } catch (ex) { console.log(ex); obj.close(); } } }); obj.wsClient.on('close', function () { parent.parent.debug('relay', 'RDP: Relay websocket closed'); obj.close(); }); @@ -984,6 +984,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { if ((node == null) || (visible == false) || ((rights & MESHRIGHT_REMOTECONTROL) == 0)) { obj.close(); return; } if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { obj.viewonly = true; } if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0)) { obj.limitedinput = true; } + node = parent.common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials obj.mtype = node.mtype; // Store the device group type obj.meshid = node.meshid; // Store the MeshID @@ -1281,16 +1282,14 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { ws._socket.resume(); } } else { - if (typeof data == 'string') { - // Forward any ping/pong commands to the browser + try { // Forward any ping/pong commands to the browser var cmd = null; - try { cmd = JSON.parse(data); } catch (ex) { } + cmd = JSON.parse(data); if ((cmd != null) && (cmd.ctrlChannel == '102938') && ((cmd.type == 'ping') || (cmd.type == 'pong'))) { obj.ws.send(data); } return; + } catch(ex) { // Relay WS --> SSH instead + if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } - - // Relay WS --> SSH - if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } }); obj.wsClient.on('close', function () { parent.parent.debug('relay', 'SSH: Relay websocket closed'); obj.close(); }); @@ -1318,7 +1317,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { // Check if we have SSH credentials for this device parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) { if ((err != null) || (nodes == null) || (nodes.length != 1)) return; - const node = nodes[0]; + const node = parent.common.unEscapeLinksFieldName(nodes[0]); // unEscape node data for rdp/ssh credentials if ((domain.allowsavingdevicecredentials === false) || (node.ssh == null) || (typeof node.ssh != 'object') || (node.ssh[obj.userid] == null) || (typeof node.ssh[obj.userid].u != 'string') || ((typeof node.ssh[obj.userid].p != 'string') && (typeof node.ssh[obj.userid].k != 'string'))) { // Send a request for SSH authentication try { ws.send(JSON.stringify({ action: 'sshauth' })) } catch (ex) { } @@ -1366,7 +1365,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { obj.termSize = msg; parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) { if ((err != null) || (nodes == null) || (nodes.length != 1)) return; - const node = nodes[0]; + const node = parent.common.unEscapeLinksFieldName(nodes[0]); // unEscape node data for rdp/ssh credentials if (node.ssh != null) { obj.username = node.ssh.u; obj.privateKey = node.ssh.k; @@ -1408,7 +1407,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) { parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) { if (obj.cookie == null) return; // obj has been cleaned up, just exit. if ((err != null) || (nodes == null) || (nodes.length != 1)) { parent.parent.debug('relay', 'SSH: Invalid device'); obj.close(); } - const node = nodes[0]; + const node = parent.common.unEscapeLinksFieldName(nodes[0]); // unEscape node data for rdp/ssh credentials obj.nodeid = node._id; // Store the NodeID obj.meshid = node.meshid; // Store the MeshID obj.mtype = node.mtype; // Store the device group type @@ -1621,16 +1620,14 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u ws._socket.resume(); } } else { - if (typeof data == 'string') { - // Forward any ping/pong commands to the browser + try { // Forward any ping/pong commands to the browser var cmd = null; - try { cmd = JSON.parse(data); } catch (ex) { } + cmd = JSON.parse(data); if ((cmd != null) && (cmd.ctrlChannel == '102938') && ((cmd.type == 'ping') || (cmd.type == 'pong'))) { try { obj.ws.send(data); } catch (ex) { console.log(ex); } } return; + } catch (ex) { // Relay WS --> SSH + if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } - - // Relay WS --> SSH - if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } }); obj.wsClient.on('close', function () { @@ -1743,6 +1740,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u if ((user == null) || (req.query.nodeid == null)) { obj.close(); return; } // Invalid nodeid parent.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) { if (obj.ws == null) return; // obj has been cleaned up, just exit. + node = parent.common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials // Check permissions if ((rights & 8) == 0) { obj.close(); return; } // No MESHRIGHT_REMOTECONTROL rights @@ -1969,16 +1967,15 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user ws._socket.resume(); } } else { - if (typeof data == 'string') { + try { // Forward any ping/pong commands to the browser var cmd = null; - try { cmd = JSON.parse(data); } catch (ex) { } + cmd = JSON.parse(data); if ((cmd != null) && (cmd.ctrlChannel == '102938') && ((cmd.type == 'ping') || (cmd.type == 'pong'))) { obj.ws.send(data); } return; + } catch (ex) { // Relay WS --> SSH + if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } - - // Relay WS --> SSH - if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } } }); obj.wsClient.on('close', function () { @@ -2273,6 +2270,7 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user if ((user == null) || (req.query.nodeid == null)) { obj.close(); return; } // Invalid nodeid parent.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) { if (obj.ws == null) return; // obj has been cleaned up, just exit. + node = parent.common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials // Check permissions if ((rights & 8) == 0) { obj.close(); return; } // No MESHRIGHT_REMOTECONTROL rights diff --git a/authenticode.js b/authenticode.js index 821cf7ae..5f0bd441 100644 --- a/authenticode.js +++ b/authenticode.js @@ -298,7 +298,7 @@ function createAuthenticodeHandler(path) { for (var i = 0; i < obj.header.coff.numberOfSections; i++) { var section = {}; buf = readFileSlice(obj.header.SectionHeadersPtr + (i * 40), 40); - if (buf[0] != 46) { obj.close(); return false; }; // Name of the section must start with a dot. If not, something is wrong. + if ((buf[0] != 46) && (buf[0] != 95)) { obj.close(); return false; }; // Name of the section must start with a dot or underscore. If not, something is wrong. var sectionName = buf.slice(0, 8).toString().trim('\0'); var j = sectionName.indexOf('\0'); if (j >= 0) { sectionName = sectionName.substring(0, j); } // Trim any trailing zeroes @@ -1566,7 +1566,11 @@ function createAuthenticodeHandler(path) { options.protocol = timeServerUrl.protocol; options.hostname = timeServerUrl.hostname; 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) { // No proxy needed @@ -1584,7 +1588,7 @@ function createAuthenticodeHandler(path) { // Set up the request var responseAccumulator = ''; - var req = require('http').request(options, function (res) { + var req = http.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { responseAccumulator += chunk; }); res.on('end', function () { func(null, responseAccumulator); }); @@ -1605,12 +1609,12 @@ function createAuthenticodeHandler(path) { proxyOptions.protocol = proxyUrl.protocol; proxyOptions.hostname = proxyUrl.hostname; 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 var responseAccumulator = ''; - var req = require('http').request(proxyOptions); + var req = http.request(proxyOptions); req.on('error', function (err) { func('' + err); }); req.on('connect', function (res, socket, head) { // Make a request over the HTTP tunnel diff --git a/certoperations.js b/certoperations.js index 40447ab0..50c92f2a 100644 --- a/certoperations.js +++ b/certoperations.js @@ -1049,6 +1049,7 @@ module.exports.CertificateOperations = function (parent) { config.domains[i].certs = r.dns[i]; } else { console.log("WARNING: File \"webserver-" + i + "-cert-public.crt\" missing, domain \"" + i + "\" will not work correctly."); + rcountmax++; } } else { // If the web certificate already exist, load it. Load both certificate and private key @@ -1407,20 +1408,15 @@ module.exports.CertificateOperations = function (parent) { // Perform any general operation obj.acceleratorPerformOperation = function (operation, data, tag, func) { - if (acceleratorTotalCount <= 1) { - // No accelerators available - require(program).processMessage({ action: operation, data: data, tag: tag, func: func }); + var acc = obj.getAccelerator(); + if (acc == null) { + // Add to pending accelerator workload + acceleratorPerformSignaturePushFuncCall++; + pendingAccelerator.push({ action: operation, data: data, tag: tag, func: func }); } else { - var acc = obj.getAccelerator(); - if (acc == null) { - // Add to pending accelerator workload - acceleratorPerformSignaturePushFuncCall++; - pendingAccelerator.push({ action: operation, data: data, tag: tag, func: func }); - } else { - // Send to accelerator now - acceleratorPerformSignatureRunFuncCall++; - acc.send(acc.x = { action: operation, data: data, tag: tag, func: func }); - } + // Send to accelerator now + acceleratorPerformSignatureRunFuncCall++; + acc.send(acc.x = { action: operation, data: data, tag: tag, func: func }); } }; diff --git a/common.js b/common.js index 93e6fd5c..f1fdf105 100644 --- a/common.js +++ b/common.js @@ -155,14 +155,26 @@ module.exports.objKeysToLower = function (obj, exceptions, parent) { return obj; }; -// Escape and unescape feild names so there are no invalid characters for MongoDB -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.unEscapeFieldName = function (name) { if (name.indexOf('%') == -1) return name; return name.split('%2E').join('.').split('%24').join('$').split('%25').join('%'); }; +// 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) && (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('%2C').join(',').split('%2E').join('.').split('%24').join('$').split('%25').join('%'); }; -// Escape all links -module.exports.escapeLinksFieldNameEx = function (docx) { if (docx.links == null) { return docx; } var doc = Object.assign({}, docx); doc.links = Object.assign({}, doc.links); for (var i in doc.links) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.links[ue] = doc.links[i]; delete doc.links[i]; } } return doc; }; -module.exports.escapeLinksFieldName = function (docx) { var doc = Object.assign({}, docx); if (doc.links != null) { doc.links = Object.assign({}, doc.links); for (var i in doc.links) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.links[ue] = doc.links[i]; delete doc.links[i]; } } } return doc; }; -module.exports.unEscapeLinksFieldName = function (doc) { if (doc.links != null) { for (var j in doc.links) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.links[ue] = doc.links[j]; delete doc.links[j]; } } } return doc; }; +// Escape all links, SSH and RDP usernames +// 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.escapeLinksFieldName = function (docx) { + var doc = Object.assign({}, docx); + if (doc.links != null) { doc.links = Object.assign({}, doc.links); for (var i in doc.links) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.links[ue] = doc.links[i]; delete doc.links[i]; } } } + if (doc.ssh != null) { doc.ssh = Object.assign({}, doc.ssh); for (var i in doc.ssh) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.ssh[ue] = doc.ssh[i]; delete doc.ssh[i]; } } } + if (doc.rdp != null) { doc.rdp = Object.assign({}, doc.rdp); for (var i in doc.rdp) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.rdp[ue] = doc.rdp[i]; delete doc.rdp[i]; } } } + return doc; +}; +module.exports.unEscapeLinksFieldName = function (doc) { + if (doc.links != null) { for (var j in doc.links) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.links[ue] = doc.links[j]; delete doc.links[j]; } } } + if (doc.ssh != null) { for (var j in doc.ssh) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.ssh[ue] = doc.ssh[j]; delete doc.ssh[j]; } } } + if (doc.rdp != null) { for (var j in doc.rdp) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.rdp[ue] = doc.rdp[j]; delete doc.rdp[j]; } } } + return doc; +}; //module.exports.escapeAllLinksFieldName = function (docs) { for (var i in docs) { module.exports.escapeLinksFieldName(docs[i]); } return docs; }; module.exports.unEscapeAllLinksFieldName = function (docs) { for (var i in docs) { docs[i] = module.exports.unEscapeLinksFieldName(docs[i]); } return docs; }; @@ -322,6 +334,11 @@ module.exports.meshServerRightsArrayToNumber = function (val) { if (r == 'locked') { newAccRights |= 32; } if (r == 'nonewgroups') { newAccRights |= 64; } if (r == 'notools') { newAccRights |= 128; } + if (r == 'usergroups') { newAccRights |= 256; } + if (r == 'recordings') { newAccRights |= 512; } + if (r == 'locksettings') { newAccRights |= 1024; } + if (r == 'allevents') { newAccRights |= 2048; } + if (r == 'nonewdevices') { newAccRights |= 4096; } } return newAccRights; } @@ -374,4 +391,32 @@ module.exports.moveOldFiles = function (filelist) { for (var i in filelist) { if (fs.existsSync(filelist[i] + oldFileExt) == true) { extOk = false; } } } while (extOk == false); for (var i in filelist) { try { fs.renameSync(filelist[i], filelist[i] + oldFileExt); } catch (ex) { } } +} + +// Convert strArray to Array, returns array if strArray or null if any other type +module.exports.convertStrArray = function (object, split) { + if (split && typeof object === 'string') { + return object.split(split) + } else if (typeof object === 'string') { + return Array(object); + } else if (Array.isArray(object)) { + return object + } else { + return [] + } +} + +module.exports.uniqueArray = function (a) { + var seen = {}; + var out = []; + var len = a.length; + var j = 0; + for(var i = 0; i < len; i++) { + var item = a[i]; + if(seen[item] !== 1) { + seen[item] = 1; + out[j++] = item; + } + } + return out; } \ No newline at end of file diff --git a/db.js b/db.js index 42c286eb..81ea222c 100644 --- a/db.js +++ b/db.js @@ -30,8 +30,33 @@ module.exports.CreateDB = function (parent, func) { var Datastore = null; var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days) var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days) - var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire power events after 30 days (2592000). (Seconds * Minutes * Hours * Days) + var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days) const common = require('./common.js'); + const path = require('path'); + const fs = require('fs'); + const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8; + const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command + let databaseName = 'meshcentral'; + let datapathParentPath = path.dirname(parent.datapath); + let datapathFoldername = path.basename(parent.datapath); + const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental']; + const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra']; + obj.sqliteConfig = { + maintenance: '', + startupVacuum: false, + autoVacuum: 'full', + incrementalVacuum: 100, + journalMode: 'delete', + journalSize: 4096000, + synchronous: 'full', + }; + obj.performingBackup = false; + const BACKUPFAIL_ZIPCREATE = 0x0001; + const BACKUPFAIL_ZIPMODULE = 0x0010; + const BACKUPFAIL_DBDUMP = 0x0100; + obj.backupStatus = 0x0; + obj.newAutoBackupFile = null; + obj.newDBDumpFile = null; obj.identifier = null; obj.dbKey = null; obj.dbRecordsEncryptKey = null; @@ -105,16 +130,17 @@ module.exports.CreateDB = function (parent, func) { // Perform database maintenance obj.maintenance = function () { - if (obj.databaseType == 1) { // NeDB will not remove expired records unless we try to access them. This will force the removal. + parent.debug('db', 'Entering database maintenance'); + if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal. obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { // MariaDB or MySQL + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past - } else if (obj.databaseType == 7) { // AceBase + } else if (obj.databaseType == DB_ACEBASE) { // AceBase //console.log('Performing AceBase maintenance'); obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () { obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () { @@ -123,8 +149,22 @@ module.exports.CreateDB = function (parent, func) { }); }); }); - } else if (obj.databaseType == 8) { // SQLite3 - // TODO + } else if (obj.databaseType == DB_SQLITE) { // SQLite3 + //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes + obj.file.serialize(function () { + obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]); + obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]); + obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]); + obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]); + obj.file.exec(obj.sqliteConfig.maintenance, function (err) { + if (err) {console.log('Maintenance error: ' + err.message)}; + if (parent.config.settings.debug) { + sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) { + parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue); + }); + }; + }); + }); } obj.removeInactiveDevices(); } @@ -136,7 +176,7 @@ module.exports.CreateDB = function (parent, func) { for (var i in parent.config.domains) { if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') { var v = parent.config.domains[i].autoremoveinactivedevices; - if ((v > 1) && (v <= 2000)) { + if ((v >= 1) && (v <= 2000)) { if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; } removeInactiveDevicesPerDomain[i] = v; minRemoveInactiveDevicesPerDomain[i] = v; @@ -148,7 +188,7 @@ module.exports.CreateDB = function (parent, func) { for (var i in parent.webserver.meshes) { if (typeof parent.webserver.meshes[i].expireDevs == 'number') { var v = parent.webserver.meshes[i].expireDevs; - if ((v > 1) && (v <= 2000)) { + if ((v >= 1) && (v <= 2000)) { if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; } if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) { minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v; @@ -245,18 +285,18 @@ module.exports.CreateDB = function (parent, func) { obj.removeDomain = function (domainName, func) { var pendingCalls; // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now. - if (obj.databaseType == 7) { + if (obj.databaseType == DB_ACEBASE) { // AceBase pendingCalls = 3; obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } }); obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } }); obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } }); - } else if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) { + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MariaDB, MySQL or PostgreSQL pendingCalls = 2; sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } }); sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } }); - } else if (obj.databaseType == 3) { + } else if (obj.databaseType == DB_MONGODB) { // MongoDB pendingCalls = 3; obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } }); @@ -276,17 +316,17 @@ module.exports.CreateDB = function (parent, func) { // TODO: Remove all meshes that dont have any links // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now. - if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) { + if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MariaDB, MySQL or PostgreSQL obj.RemoveAllOfType('event', function () { }); obj.RemoveAllOfType('power', function () { }); obj.RemoveAllOfType('smbios', function () { }); - } else if (obj.databaseType == 3) { + } else if (obj.databaseType == DB_MONGODB) { // MongoDB obj.file.deleteMany({ type: 'event' }, { multi: true }); obj.file.deleteMany({ type: 'power' }, { multi: true }); obj.file.deleteMany({ type: 'smbios' }, { multi: true }); - } else if ((obj.databaseType == 1) || (obj.databaseType == 2)) { + } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) { // NeDB or MongoJS obj.file.remove({ type: 'event' }, { multi: true }); obj.file.remove({ type: 'power' }, { multi: true }); @@ -387,19 +427,19 @@ module.exports.CreateDB = function (parent, func) { if (meshChange) { obj.Set(docs[i]); } } } - if (obj.databaseType == 8) { + if (obj.databaseType == DB_SQLITE) { // SQLite - } else if (obj.databaseType == 7) { + } else if (obj.databaseType == DB_ACEBASE) { // AceBase - } else if (obj.databaseType == 6) { + } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { }); - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { }); - } else if (obj.databaseType == 3) { + } else if (obj.databaseType == DB_MONGODB) { // MongoDB obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true }); } else { @@ -417,14 +457,66 @@ module.exports.CreateDB = function (parent, func) { }; // Get encryption key - obj.getEncryptDataKey = function (password) { + obj.getEncryptDataKey = function (password, salt, iterations) { if (typeof password != 'string') return null; - return parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32); + let key; + try { + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384'); + } catch (ex) { + // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default. + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32); + } + return key } // Encrypt data obj.encryptData = function (password, plaintext) { - var key = obj.getEncryptDataKey(password); + let encryptionVersion = 0x01; + let iterations = 100000 + const iv = parent.crypto.randomBytes(16); + var key = obj.getEncryptDataKey(password, iv, iterations); + if (key == null) return null; + const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv); + var ciphertext = aes.update(plaintext); + let versionbuf = Buffer.allocUnsafe(2); + versionbuf.writeUInt16BE(encryptionVersion); + let iterbuf = Buffer.allocUnsafe(4); + iterbuf.writeUInt32BE(iterations); + let encryptedBuf = aes.final(); + ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]); + return ciphertext.toString('base64'); + } + + // Decrypt data + obj.decryptData = function (password, ciphertext) { + // Adding an encryption version lets us avoid try catching in the future + let ciphertextBytes = Buffer.from(ciphertext, 'base64'); + let encryptionVersion = ciphertextBytes.readUInt16BE(0); + try { + switch (encryptionVersion) { + case 0x01: + let iterations = ciphertextBytes.readUInt32BE(2); + let authTag = ciphertextBytes.slice(6, 22); + const iv = ciphertextBytes.slice(22, 38); + const data = ciphertextBytes.slice(38); + let key = obj.getEncryptDataKey(password, iv, iterations); + if (key == null) return null; + const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv); + aes.setAuthTag(authTag); + let plaintextBytes = Buffer.from(aes.update(data)); + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]); + return plaintextBytes; + default: + return obj.oldDecryptData(password, ciphertextBytes); + } + } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); } + } + + // Encrypt data + // The older encryption system uses CBC without integraty checking. + // This method is kept only for testing + obj.oldEncryptData = function (password, plaintext) { + let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32); if (key == null) return null; const iv = parent.crypto.randomBytes(16); const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv); @@ -433,16 +525,17 @@ module.exports.CreateDB = function (parent, func) { return ciphertext.toString('base64'); } - // Decrypt data - obj.decryptData = function (password, ciphertext) { + // Decrypt data + // The older encryption system uses CBC without integraty checking. + // This method is kept only to convert the old encryption to the new one. + obj.oldDecryptData = function (password, ciphertextBytes) { + if (typeof password != 'string') return null; try { - var key = obj.getEncryptDataKey(password); - if (key == null) return null; - const ciphertextBytes = Buffer.from(ciphertext, 'base64'); const iv = ciphertextBytes.slice(0, 16); const data = ciphertextBytes.slice(16); + let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32); const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv); - var plaintextBytes = Buffer.from(aes.update(data)); + let plaintextBytes = Buffer.from(aes.update(data)); plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]); return plaintextBytes; } catch (ex) { return null; } @@ -451,33 +544,33 @@ module.exports.CreateDB = function (parent, func) { // Get the number of records in the database for various types, this is the slow NeDB way. // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database. obj.getStats = function (func) { - if (obj.databaseType == 7) { + if (obj.databaseType == DB_ACEBASE) { // AceBase // TODO - } else if (obj.databaseType == 6) { + } else if (obj.databaseType == DB_POSTGRESQL) { // PostgreSQL // TODO - } else if (obj.databaseType == 5) { + } else if (obj.databaseType == DB_MYSQL) { // MySQL // TODO - } else if (obj.databaseType == 4) { + } else if (obj.databaseType == DB_MARIADB) { // MariaDB // TODO - } else if (obj.databaseType == 3) { + } else if (obj.databaseType == DB_MONGODB) { // MongoDB obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) { var counters = {}, totalCount = 0; if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } } func(counters); }); - } else if (obj.databaseType == 2) { + } else if (obj.databaseType == DB_MONGOJS) { // MongoJS obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) { var counters = {}, totalCount = 0; if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } } func(counters); }); - } else if (obj.databaseType == 1) { + } else if (obj.databaseType == DB_NEDB) { // NeDB version obj.file.count({ type: 'node' }, function (err, nodeCount) { obj.file.count({ type: 'mesh' }, function (err, meshCount) { @@ -517,8 +610,8 @@ module.exports.CreateDB = function (parent, func) { if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } } obj.GetAllType('mesh', function (err, docs) { if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } } - if (obj.databaseType == 1) { // If we are using NeDB, compact the database. - obj.file.persistence.compactDatafile(); + if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database. + obj.file.compactDatafile(); obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt. } else { func(count); // For all other databases, normal exit. @@ -668,13 +761,31 @@ module.exports.CreateDB = function (parent, func) { if (parent.args.sqlite3) { // SQLite3 database setup - obj.databaseType = 8; + obj.databaseType = DB_SQLITE; const sqlite3 = require('sqlite3'); - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), sqlite3.OPEN_READWRITE, function (err) { + let configParams = parent.config.settings.sqlite3; + if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';}; + obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false; + obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental'; + obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100; + obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete'; + //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'? + if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'}; + obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600; + //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous + obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full'; + if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'}; + if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'}; + obj.sqliteConfig.maintenance += 'PRAGMA optimize;'; + + parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4)); + if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') }; + //.cached not usefull + obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) { if (err && (err.code == 'SQLITE_CANTOPEN')) { // Database needs to be created - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), function (err) { - if (err) { console.log("SQLite Error: " + err); exit(1); return; } + obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) { + if (err) { console.log("SQLite Error: " + err); process.exit(1); } obj.file.exec(` CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON); CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON); @@ -696,20 +807,24 @@ module.exports.CreateDB = function (parent, func) { CREATE INDEX ndxsmbiostime ON smbios (time); CREATE INDEX ndxsmbiosexpire ON smbios (expire); `, function (err) { - // Completed setup of SQLite3 + // Completed DB creation of SQLite3 + sqliteSetOptions(func); + //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity setupFunctions(func); } ); }); return; - } else if (err) { console.log("SQLite Error: " + err); exit(1); return; } + } else if (err) { console.log("SQLite Error: " + err); process.exit(0); } - // Completed setup of SQLite3 + //for existing db's + sqliteSetOptions(); + //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity setupFunctions(func); }); } else if (parent.args.acebase) { // AceBase database setup - obj.databaseType = 7; + obj.databaseType = DB_ACEBASE; const { AceBase } = require('acebase'); // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100 obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } }); @@ -733,27 +848,39 @@ module.exports.CreateDB = function (parent, func) { }); } else if (parent.args.mariadb || parent.args.mysql) { var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql; - var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral'; + if (typeof connectinArgs == 'string') { + const parts = connectinArgs.split(/[:@/]+/); + var connectionObject = { + "user": parts[1], + "password": parts[2], + "host": parts[3], + "port": parts[4], + "database": parts[5] + }; + var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral'; + } else { + var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral'; - // Including the db name in the connection obj will cause a connection faliure if it does not exist - var connectionObject = Clone(connectinArgs); - delete connectionObject.database; + // Including the db name in the connection obj will cause a connection faliure if it does not exist + var connectionObject = Clone(connectinArgs); + delete connectionObject.database; - try { - if (connectinArgs.ssl) { - if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } }; - if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; } - if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; } - if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; } + try { + if (connectinArgs.ssl) { + if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } }; + if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; } + if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; } + if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; } + } + } catch (ex) { + console.log('Error loading SQL Connector certificate: ' + ex); + process.exit(); } - } catch (ex) { - console.log('Error loading SQL Connector certificate: ' + ex); - process.exit(); } if (parent.args.mariadb) { // Use MariaDB - obj.databaseType = 4; + obj.databaseType = DB_MARIADB; var tempDatastore = require('mariadb').createPool(connectionObject); tempDatastore.getConnection().then(function (conn) { conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) { @@ -767,7 +894,7 @@ module.exports.CreateDB = function (parent, func) { createTablesIfNotExist(dbname); } else if (parent.args.mysql) { // Use MySQL - obj.databaseType = 5; + obj.databaseType = DB_MYSQL; var tempDatastore = require('mysql2').createPool(connectionObject); tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) { if (error != null) { @@ -781,35 +908,56 @@ module.exports.CreateDB = function (parent, func) { } } else if (parent.args.postgres) { // Postgres SQL - var connectinArgs = parent.args.postgres; - var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral'; - delete connectinArgs.database; - obj.databaseType = 6; - const pgtools = require('pgtools'); - pgtools.createdb(connectinArgs, dbname, function (err, res) { - const { Pool, Client } = require('pg'); - connectinArgs.database = dbname; - Datastore = new Client(connectinArgs); - Datastore.connect(); - if (err == null) { - // Create the tables and indexes - postgreSqlCreateTables(func); - } else { - // Database already existed, perform a test query to see if the main table is present - sqlDbQuery('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, docs) { - if (err == null) { setupFunctions(func); } else { postgreSqlCreateTables(func); } // If not present, create the tables and indexes + let connectinArgs = parent.args.postgres; + connectinArgs.database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral'); + + let DatastoreTest; + obj.databaseType = DB_POSTGRESQL; + const { Client } = require('pg'); + Datastore = new Client(connectinArgs); + //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist' + connectinArgs.database = 'postgres'; + DatastoreTest = new Client(connectinArgs); + DatastoreTest.connect(); + + DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating + if (res.rowCount != 0) { // database exists now check tables exists + DatastoreTest.end(); + Datastore.connect(); + Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) { + if (err == null) { + (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func) + } else + if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html + postgreSqlCreateTables(func); + } else { + console.log('Postgresql database exists, other error: ', err.message); process.exit(0); + }; + }); + } else { // If not present, create the tables and indexes + //not needed, just use a create db statement: const pgtools = require('pgtools'); + DatastoreTest.query('CREATE DATABASE '+ databaseName + ';', [], function (err, res) { + if (err == null) { + // Create the tables and indexes + DatastoreTest.end(); + Datastore.connect(); + postgreSqlCreateTables(func); + } else { + console.log('Postgresql database create error: ', err.message); + process.exit(0); + } }); } }); } else if (parent.args.mongodb) { // Use MongoDB - obj.databaseType = 3; + obj.databaseType = DB_MONGODB; // If running an older NodeJS version, TextEncoder/TextDecoder is required if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; } if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; } - require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true }, function (err, client) { + require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) { if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; } Datastore = client; parent.debug('db', 'Connected to MongoDB database...'); @@ -827,7 +975,7 @@ module.exports.CreateDB = function (parent, func) { } else { if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) { // We are running with mongoDB older than 3.6, this is not good. - parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better."); + parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true); } } }); @@ -986,7 +1134,7 @@ module.exports.CreateDB = function (parent, func) { }); } else if (parent.args.xmongodb) { // Use MongoJS, this is the old system. - obj.databaseType = 2; + obj.databaseType = DB_MONGOJS; Datastore = require('mongojs'); var db = Datastore(parent.args.xmongodb); var dbcollection = 'meshcentral'; @@ -1090,9 +1238,12 @@ module.exports.CreateDB = function (parent, func) { setupFunctions(func); // Completed setup of MongoJS } else { // Use NeDB (The default) - obj.databaseType = 1; - try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies. - if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it. + obj.databaseType = DB_NEDB; + try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support. + if (Datastore == null) { + try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies. + if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it. + } var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true }; // If a DB encryption key is provided, perform database encryption @@ -1119,7 +1270,7 @@ module.exports.CreateDB = function (parent, func) { // Start NeDB main collection and setup indexes obj.file = new Datastore(datastoreOptions); - obj.file.persistence.setAutocompactionInterval(86400000); // Compact once a day + obj.file.setAutocompactionInterval(86400000); // Compact once a day obj.file.ensureIndex({ fieldName: 'type' }); obj.file.ensureIndex({ fieldName: 'domain' }); obj.file.ensureIndex({ fieldName: 'meshid', sparse: true }); @@ -1128,7 +1279,7 @@ module.exports.CreateDB = function (parent, func) { // Setup the events collection and setup indexes obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 }); - obj.eventsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day + obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field. obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true }); obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds }); @@ -1136,18 +1287,18 @@ module.exports.CreateDB = function (parent, func) { // Setup the power collection and setup indexes obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 }); - obj.powerfile.persistence.setAutocompactionInterval(86400000); // Compact once a day + obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day obj.powerfile.ensureIndex({ fieldName: 'nodeid' }); obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds }); obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones. //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 }); - parent.fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { }); + fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { }); // Setup the server stats collection and setup indexes obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 }); - obj.serverstatsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day + obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds }); obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events @@ -1155,12 +1306,51 @@ module.exports.CreateDB = function (parent, func) { // Setup plugin info collection if (obj.pluginsActive) { obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true }); - obj.pluginsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day + obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day } setupFunctions(func); // Completed setup of NeDB } + function sqliteSetOptions(func) { + //get current auto_vacuum mode for comparison + obj.file.get('PRAGMA auto_vacuum;', function(err, current){ + let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' + + 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' + + 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' + + 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' + + 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' + + 'PRAGMA optimize=0x10002;'; + //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum + if ( obj.sqliteConfig.startupVacuum + || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none') + || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none')) + { + pragma += 'VACUUM;'; + }; + parent.debug ('db', 'Config statement: ' + pragma); + + obj.file.exec( pragma, + function (err) { + if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) }; + sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) { + parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue); + }); + }); + }); + //setupFunctions(func); + } + + function sqliteGetPragmas (pragmas, func){ + //pragmas can only be gotting one by one + pragmas.forEach (function (pragma) { + obj.file.get('PRAGMA ' + pragma + ';', function(err, res){ + if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] }; + if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] }; + if (func) { func (pragma, res[pragma]); } + }); + }); + } // Create the PostgreSQL tables function postgreSqlCreateTables(func) { // Database was created, create the tables @@ -1202,7 +1392,7 @@ module.exports.CreateDB = function (parent, func) { // Query the database function sqlDbQuery(query, args, func, debug) { - if (obj.databaseType == 8) { // SQLite + if (obj.databaseType == DB_SQLITE) { // SQLite if (args == null) { args = []; } obj.file.all(query, args, function (err, docs) { if (err != null) { console.log(query, args, err, docs); } @@ -1217,7 +1407,7 @@ module.exports.CreateDB = function (parent, func) { } if (func) { func(err, docs); } }); - } else if (obj.databaseType == 4) { // MariaDB + } else if (obj.databaseType == DB_MARIADB) { // MariaDB Datastore.getConnection() .then(function (conn) { conn.query(query, args) @@ -1236,7 +1426,7 @@ module.exports.CreateDB = function (parent, func) { }) .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } }); }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } }); - } else if (obj.databaseType == 5) { // MySQL + } else if (obj.databaseType == DB_MYSQL) { // MySQL Datastore.query(query, args, function (error, results, fields) { if (error != null) { if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); } @@ -1257,10 +1447,9 @@ module.exports.CreateDB = function (parent, func) { if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } } } }); - } else if (obj.databaseType == 6) { // Postgres SQL + } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL Datastore.query(query, args, function (error, results) { if (error != null) { - console.log(query, args, error); if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); } } else { var docs = []; @@ -1287,7 +1476,7 @@ module.exports.CreateDB = function (parent, func) { // Exec on the database function sqlDbExec(query, args, func) { - if (obj.databaseType == 4) { // MariaDB + if (obj.databaseType == DB_MARIADB) { // MariaDB Datastore.getConnection() .then(function (conn) { conn.query(query, args) @@ -1297,7 +1486,7 @@ module.exports.CreateDB = function (parent, func) { }) .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } }); }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } }); - } else if ((obj.databaseType == 5) || (obj.databaseType == 6)) { // MySQL or Postgres SQL + } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL Datastore.query(query, args, function (error, results, fields) { if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); } }); @@ -1306,7 +1495,7 @@ module.exports.CreateDB = function (parent, func) { // Execute a batch of commands on the database function sqlDbBatchExec(queries, func) { - if (obj.databaseType == 4) { // MariaDB + if (obj.databaseType == DB_MARIADB) { // MariaDB Datastore.getConnection() .then(function (conn) { var Promises = []; @@ -1316,7 +1505,7 @@ module.exports.CreateDB = function (parent, func) { .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } }); }) .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } }); - } else if (obj.databaseType == 5) { // MySQL + } else if (obj.databaseType == DB_MYSQL) { // MySQL Datastore.getConnection(function(err, connection) { if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; } var Promises = []; @@ -1325,7 +1514,7 @@ module.exports.CreateDB = function (parent, func) { .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } }) .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } }); }); - } else if (obj.databaseType == 6) { // Postgres + } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres var Promises = []; for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } } Promise.all(Promises) @@ -1335,7 +1524,7 @@ module.exports.CreateDB = function (parent, func) { } function setupFunctions(func) { - if (obj.databaseType == 8) { + if (obj.databaseType == DB_SQLITE) { // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/ obj.Set = function (value, func) { obj.dbCounters.fileSet++; @@ -1463,9 +1652,9 @@ module.exports.CreateDB = function (parent, func) { obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } }; obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } }; obj.getLocalAmtNodes = function (func) { - sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NOT NULL)', null, function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } - var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null) { r.push(docs[i]); } } } func(err, r); + var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); }; obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { @@ -1494,33 +1683,91 @@ module.exports.CreateDB = function (parent, func) { } }); }; - obj.GetEvents = function (ids, domain, func) { + obj.GetEvents = function (ids, domain, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1) ORDER BY time DESC', [domain], func); + query = query + "WHERE (domain = $1"; + if (filter != null) { + query = query + " AND action = $2"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))) GROUP BY id ORDER BY time DESC', [domain], func); + query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))'; + if (filter != null) { + query = query + " AND action = $2"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC "; } + sqlDbQuery(query, dataarray, func); }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1) ORDER BY time DESC LIMIT $2', [domain, limit], func); + query = query + "WHERE (domain = $1"; + if (filter != null) { + query = query + " AND action = $2) ORDER BY time DESC LIMIT $3"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $2"; + } } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))) GROUP BY id ORDER BY time DESC LIMIT $2', [domain, limit], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))"; + if (filter != null) { + query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3"; + dataarray.push(filter); + } else { + query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2"; + } } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEvents = function (ids, domain, userid, func) { + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1 AND userid = $2) ORDER BY time DESC', [domain, userid], func); + query = query + "WHERE (domain = $1 AND userid = $2"; + if (filter != null) { + query = query + " AND action = $3"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))) GROUP BY id ORDER BY time DESC', [domain, userid], func); + query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))'; + if (filter != null) { + query = query + " AND action = $3"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC"; } + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1 AND userid = $2) ORDER BY time DESC LIMIT $3', [domain, userid, limit], func); + query = query + "WHERE (domain = $1 AND userid = $2"; + if (filter != null) { + query = query + " AND action = $3) ORDER BY time DESC LIMIT $4"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $3"; + } } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))) GROUP BY id ORDER BY time DESC LIMIT $3', [domain, userid, limit], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))"; + if (filter != null) { + query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4"; + dataarray.push(filter); + } else { + query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3"; + } } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { if (ids.indexOf('*') >= 0) { @@ -1530,8 +1777,30 @@ module.exports.CreateDB = function (parent, func) { } }; //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) ORDER BY time DESC LIMIT $3', [nodeid, domain, limit], func); }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ORDER BY time DESC LIMIT $4', [nodeid, domain, userid, limit], func); }; + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2"; + var dataarray = [nodeid, domain]; + if (filter != null) { + query = query + " AND action = $3) ORDER BY time DESC LIMIT $4"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $3"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) "; + var dataarray = [nodeid, domain, userid]; + if (filter != null) { + query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5"; + dataarray.push(filter); + } else { + query = query + "ORDER BY time DESC LIMIT $4"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); }; obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); }; @@ -1581,7 +1850,7 @@ module.exports.CreateDB = function (parent, func) { obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); }; obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); }; } - } else if (obj.databaseType == 7) { + } else if (obj.databaseType == DB_ACEBASE) { // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase obj.Set = function (data, func) { data = common.escapeLinksFieldNameEx(data); @@ -1669,42 +1938,78 @@ module.exports.CreateDB = function (parent, func) { obj.dbCounters.eventsSet++; obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } }); }; - obj.GetEvents = function (ids, domain, func) { + obj.GetEvents = function (ids, domain, filter, func) { // This request is slow since we have not found a .filter() that will take two arrays and match a single item. - obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { - const docs = []; - for (var i in snapshots) { - const doc = snapshots[i].val(); - if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; - var found = false; - for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches - if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } - } - func(null, docs); - }); + if (filter != null) { + obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { + const docs = []; + for (var i in snapshots) { + const doc = snapshots[i].val(); + if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; + var found = false; + for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches + if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } + } + func(null, docs); + }); + } else { + obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { + const docs = []; + for (var i in snapshots) { + const doc = snapshots[i].val(); + if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; + var found = false; + for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches + if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } + } + func(null, docs); + }); + } }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { // This request is slow since we have not found a .filter() that will take two arrays and match a single item. // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter: // obj.file.indexes.create('events', 'ids', { type: 'array' }); // db.query('events').filter('ids', 'array:contains-one-of', ids) - obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { - const docs = []; - for (var i in snapshots) { - const doc = snapshots[i].val(); - if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; - var found = false; - for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches - if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } - } - func(null, docs); - }); + if (filter != null) { + obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { + const docs = []; + for (var i in snapshots) { + const doc = snapshots[i].val(); + if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; + var found = false; + for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches + if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } + } + func(null, docs); + }); + } else { + obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) { + const docs = []; + for (var i in snapshots) { + const doc = snapshots[i].val(); + if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue; + var found = false; + for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches + if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); } + } + func(null, docs); + }); + } }; - obj.GetUserEvents = function (ids, domain, userid, func) { - obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + if (filter != null) { + obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } else { + obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { - obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + if (filter != null) { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } else { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); @@ -1712,10 +2017,19 @@ module.exports.CreateDB = function (parent, func) { obj.GetUserLoginEvents = function (domain, userid, func) { obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { - obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + if (filter != null) { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } else { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + if (filter != null) { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } else { + obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); + } obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; obj.RemoveAllEvents = function (domain) { @@ -1828,7 +2142,7 @@ module.exports.CreateDB = function (parent, func) { obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) }; obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) }; } - } else if (obj.databaseType == 6) { + } else if (obj.databaseType == DB_POSTGRESQL) { // Database actions on the main collection (Postgres) obj.Set = function (value, func) { obj.dbCounters.fileSet++; @@ -1894,7 +2208,7 @@ module.exports.CreateDB = function (parent, func) { obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); }; obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } }; obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } }; - obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NOT NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null) { r.push(docs[i]); } } } func(err, r); }); }; + obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); }; obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); }; obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } } @@ -1903,6 +2217,7 @@ module.exports.CreateDB = function (parent, func) { obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) { + if(func){ func(); } if (docs.id) { for (var i in event.ids) { if (event.ids[i] != '*') { @@ -1913,33 +2228,98 @@ module.exports.CreateDB = function (parent, func) { } }); }; - obj.GetEvents = function (ids, domain, func) { + obj.GetEvents = function (ids, domain, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1) ORDER BY time DESC', [domain], func); + query = query + "WHERE (domain = $1"; + if (filter != null) { + query = query + " AND action = $2"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))) GROUP BY id ORDER BY time DESC', [domain, ids], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = $3"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC"; } + sqlDbQuery(query, dataarray, func); }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1) ORDER BY time DESC LIMIT $2', [domain, limit], func); + query = query + "WHERE (domain = $1"; + if (filter != null) { + query = query + " AND action = $2) ORDER BY time DESC LIMIT $3"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $2"; + } } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))) GROUP BY id ORDER BY time DESC LIMIT $3', [domain, ids, limit], func); + if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = $3) ORDER BY time DESC LIMIT $4"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $3"; + } } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEvents = function (ids, domain, userid, func) { + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1 AND userid = $2) ORDER BY time DESC', [domain, userid], func); + query = query + "WHERE (domain = $1 AND userid = $2"; + if (filter != null) { + query = query + " AND action = $3"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))) GROUP BY id ORDER BY time DESC', [domain, userid, ids], func); + if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = $4"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC"; } + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = $1 AND userid = $2) ORDER BY time DESC LIMIT $3', [domain, userid, limit], func); + query = query + "WHERE (domain = $1 AND userid = $2"; + if (filter != null) { + query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 "; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $3"; + } } else { - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))) GROUP BY id ORDER BY time DESC LIMIT $4', [domain, userid, ids, limit], func); + if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. + query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5"; + dataarray.push(filter); + } else { + query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4"; + } } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { if (ids.indexOf('*') >= 0) { @@ -1949,8 +2329,30 @@ module.exports.CreateDB = function (parent, func) { } }; //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) ORDER BY time DESC LIMIT $3', [nodeid, domain, limit], func); }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ORDER BY time DESC LIMIT $4', [nodeid, domain, userid, limit], func); }; + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2"; + var dataarray = [nodeid, domain]; + if (filter != null) { + query = query + " AND action = $3) ORDER BY time DESC LIMIT $4"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $3"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))"; + var dataarray = [nodeid, domain, userid]; + if (filter != null) { + query = query + " AND action = $4) ORDER BY time DESC LIMIT $5"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT $4"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); }; obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); }; @@ -2000,7 +2402,7 @@ module.exports.CreateDB = function (parent, func) { obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); }; obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); }; } - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // Database actions on the main collection (MariaDB or MySQL) obj.Set = function (value, func) { obj.dbCounters.fileSet++; @@ -2066,7 +2468,7 @@ module.exports.CreateDB = function (parent, func) { obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); }; obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } }; obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } }; - obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NOT NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null) { r.push(docs[i]); } } } func(err, r); }); }; + obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); }; obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); }; obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } } @@ -2078,37 +2480,95 @@ module.exports.CreateDB = function (parent, func) { for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } } sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } }); }; - obj.GetEvents = function (ids, domain, func) { + obj.GetEvents = function (ids, domain, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = ?) ORDER BY time DESC', [domain], func); + query = query + "WHERE (domain = ?"; + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, ids], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC"; } + sqlDbQuery(query, dataarray, func); }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = ?) ORDER BY time DESC LIMIT ?', [domain, limit], func); + query = query + "WHERE (domain = ?"; + if (filter != null) { + query = query + " AND action = ? "; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC LIMIT ?"; } else { if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, ids, limit], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?"; } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEvents = function (ids, domain, userid, func) { + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = ? AND userid = ?) ORDER BY time DESC', [domain, userid], func); + query = query + "WHERE (domain = ? AND userid = ?"; + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC"; } else { if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, userid, ids], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC"; } + sqlDbQuery(query, dataarray, func); }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events "; + var dataarray = [domain, userid]; if (ids.indexOf('*') >= 0) { - sqlDbQuery('SELECT doc FROM events WHERE (domain = ? AND userid = ?) ORDER BY time DESC LIMIT ?', [domain, userid, limit], func); + query = query + "WHERE (domain = ? AND userid = ?"; + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") ORDER BY time DESC LIMIT ?"; } else { if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. - sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, userid, ids, limit], func); + query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)"; + dataarray.push(ids); + if (filter != null) { + query = query + " AND action = ?"; + dataarray.push(filter); + } + query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?"; } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { if (ids.indexOf('*') >= 0) { @@ -2119,8 +2579,30 @@ module.exports.CreateDB = function (parent, func) { } }; //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = ?) AND (domain = ?) ORDER BY time DESC LIMIT ?', [nodeid, domain, limit], func); }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { sqlDbQuery('SELECT doc FROM events WHERE (nodeid = ?) AND (domain = ?) AND ((userid = ?) OR (userid IS NULL)) ORDER BY time DESC LIMIT ?', [nodeid, domain, userid, limit], func); }; + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?"; + var dataarray = [nodeid, domain]; + if (filter != null) { + query = query + " AND action = ?) ORDER BY time DESC LIMIT ?"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT ?"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))"; + var dataarray = [nodeid, domain, userid]; + if (filter != null) { + query = query + " AND action = ?) ORDER BY time DESC LIMIT ?"; + dataarray.push(filter); + } else { + query = query + ") ORDER BY time DESC LIMIT ?"; + } + dataarray.push(limit); + sqlDbQuery(query, dataarray, func); + }; obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); }; obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); }; @@ -2170,7 +2652,7 @@ module.exports.CreateDB = function (parent, func) { obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); }; obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); }; } - } else if (obj.databaseType == 3) { + } else if (obj.databaseType == DB_MONGODB) { // Database actions on the main collection (MongoDB) // Bulk operations @@ -2355,14 +2837,38 @@ module.exports.CreateDB = function (parent, func) { obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); }; } - obj.GetEvents = function (ids, domain, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); }; - obj.GetUserEvents = function (ids, domain, userid, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); }; + obj.GetEvents = function (ids, domain, filter, func) { + var finddata = { domain: domain, ids: { $in: ids } }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); + }; + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { + var finddata = { domain: domain, ids: { $in: ids } }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); + }; + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); + }; + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); + }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); }; obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); }; - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); }; + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + var finddata = { domain: domain, nodeid: nodeid }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); + }; + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }; + if (filter != null) finddata.action = filter; + obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); + }; obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); }; obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); }; @@ -2527,38 +3033,74 @@ module.exports.CreateDB = function (parent, func) { // Database actions on the events collection obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); }; obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); }; - obj.GetEvents = function (ids, domain, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } }; - obj.GetEventsWithLimit = function (ids, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } }; - obj.GetUserEvents = function (ids, domain, userid, func) { - if (obj.databaseType == 1) { - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); + obj.GetEvents = function (ids, domain, filter, func) { + var finddata = { domain: domain, ids: { $in: ids } }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } }; - obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, func) { - if (obj.databaseType == 1) { - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); + obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) { + var finddata = { domain: domain, ids: { $in: ids } }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); + } + }; + obj.GetUserEvents = function (ids, domain, userid, filter, func) { + var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); + } else { + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); + } + }; + obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) { + var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); + } else { + obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } }; obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { - if (obj.databaseType == 1) { + if (obj.databaseType == DB_NEDB) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func); } }; obj.GetUserLoginEvents = function (domain, userid, func) { - if (obj.databaseType == 1) { + if (obj.databaseType == DB_NEDB) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func); } }; - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } }; - obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } }; + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) { + var finddata = { domain: domain, nodeid: nodeid }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); + } else { + obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); + } + }; + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) { + var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }; + if (filter != null) finddata.action = filter; + if (obj.databaseType == DB_NEDB) { + obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); + } else { + obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); + } + }; obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); }; obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); }; @@ -2567,7 +3109,7 @@ module.exports.CreateDB = function (parent, func) { // Database actions on the power collection obj.getAllPower = function (func) { obj.powerfile.find({}, func); }; obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); }; - obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } }; + obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } }; obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); }; obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); }; @@ -2645,58 +3187,94 @@ module.exports.CreateDB = function (parent, func) { // Return a human readable string with current backup configuration obj.getBackupConfig = function () { var r = '', backupPath = parent.backuppath; - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; } - var dbname = 'meshcentral'; + let dbname = 'meshcentral'; if (parent.args.mongodbname) { dbname = parent.args.mongodbname; } else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; } else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; } + else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'}; const currentDate = new Date(); const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2); - const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix; - const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile); + obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix; r += 'DB Name: ' + dbname + '\r\n'; - r += 'DB Type: ' + ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'AceBase'][obj.databaseType] + '\r\n'; + r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n'; r += 'BackupPath: ' + backupPath + '\r\n'; - r += 'newAutoBackupFile: ' + newAutoBackupFile + '\r\n'; - r += 'newAutoBackupPath: ' + newAutoBackupPath + '\r\n'; + r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n'; if (parent.config.settings.autobackup == null) { r += 'No Settings/AutoBackup\r\n'; } else { + if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) { + r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n'; + } if (parent.config.settings.autobackup.backupintervalhours != null) { - if (typeof parent.config.settings.autobackup.backupintervalhours != 'number') { r += 'Bad backupintervalhours type\r\n'; } - else { r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n'; } + r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n'; } if (parent.config.settings.autobackup.keeplastdaysbackup != null) { - if (typeof parent.config.settings.autobackup.keeplastdaysbackup != 'number') { r += 'Bad keeplastdaysbackup type\r\n'; } - else { r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n'; } + r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n'; } if (parent.config.settings.autobackup.zippassword != null) { - if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type\r\n'; } - else { r += 'ZIP Password Set\r\n'; } + r += 'ZIP Password: '; + if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; } + else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; } + else { r += 'Set\r\n'; } } if (parent.config.settings.autobackup.mongodumppath != null) { + r += 'MongoDump Path: '; if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; } - else { r += 'MongoDump Path: ' + parent.config.settings.autobackup.mongodumppath + '\r\n'; } + else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; } } if (parent.config.settings.autobackup.mysqldumppath != null) { + r += 'MySqlDump Path: '; if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; } - else { r += 'MySqlDump Path: ' + parent.config.settings.autobackup.mysqldumppath + '\r\n'; } + else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; } } + if (parent.config.settings.autobackup.backupotherfolders) { + r += 'Backup other folders: '; + r += parent.filespath + ', ' + parent.recordpath + '\r\n'; + } + if (parent.config.settings.autobackup.backupwebfolders) { + r += 'Backup webfolders: '; + if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath }; + if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath}; + if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath}; + r+= '\r\n'; + } + if (parent.config.settings.autobackup.backupignorefilesglob != []) { + r += 'Backup IgnoreFilesGlob: '; + { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; } + } + if (parent.config.settings.autobackup.backupskipfoldersglob != []) { + r += 'Backup SkipFoldersGlob: '; + { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; } + } + + if (typeof parent.config.settings.autobackup.s3 == 'object') { + r += 'S3 Backups: Enabled\r\n'; + } + if (typeof parent.config.settings.autobackup.webdav == 'object') { + r += 'WebDAV Backups: Enabled\r\n'; + r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n'; + r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n'; + } + if (typeof parent.config.settings.autobackup.googledrive == 'object') { + r += 'Google Drive Backups: Enabled\r\n'; + } + + } return r; } function buildSqlDumpCommand() { - var props = (obj.databaseType == 4) ? parent.args.mariadb : parent.args.mysql; + var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql; var mysqldumpPath = 'mysqldump'; if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) { - mysqldumpPath = parent.config.settings.autobackup.mysqldumppath; + mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath); } var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\''; @@ -2709,11 +3287,11 @@ module.exports.CreateDB = function (parent, func) { // SSL options different on mariadb/mysql var sslOptions = ''; - if (obj.databaseType == 4) { + if (obj.databaseType == DB_MARIADB) { if (props.ssl) { sslOptions = ' --ssl'; if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath; - if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-verify-server-cert'; + if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'}; if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath; if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath; } @@ -2740,7 +3318,7 @@ module.exports.CreateDB = function (parent, func) { var mongoDumpPath = 'mongodump'; if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) { - mongoDumpPath = parent.config.settings.autobackup.mongodumppath; + mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath); } var cmd = '"' + mongoDumpPath + '"'; @@ -2750,57 +3328,88 @@ module.exports.CreateDB = function (parent, func) { } // Check that the server is capable of performing a backup + // Tries configured custom location with fallback to default location + // Now runs after autobackup config init in meshcentral.js so config options are checked obj.checkBackupCapability = function (func) { - if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); } - if ((obj.databaseType == 2) || (obj.databaseType == 3)) { - // Check that we have access to MongoDump - var backupPath = parent.backuppath; - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; } - try { parent.fs.mkdirSync(backupPath); } catch (ex) { } - if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; } + if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { return; }; + //block backup until validated. Gets put back if all checks are ok. + let backupInterval = parent.config.settings.autobackup.backupintervalhours; + parent.config.settings.autobackup.backupintervalhours = -1; + let backupPath = parent.backuppath; + if (backupPath.startsWith(parent.datapath)) { + func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made."); + return; + } + // Check create/write backupdir + try { fs.mkdirSync(backupPath); } + catch (e) { + // EEXIST error = dir already exists + if (e.code != 'EEXIST' ) { + //Unable to create backuppath + console.error(e.message); + func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message); + return; + } + } + const testFile = path.join(backupPath, (parent.config.settings.autobackup.backupname + ".test")); + + try { fs.writeFileSync( testFile, "DeleteMe"); } + catch (e) { + //Unable to create file + console.error (e.message); + func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message); + return; + } + try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');} + catch (e) { + console.error (e.message); + func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message); + // Assume write rights, no delete rights. Continue with warning. + //return; + } + + // Check database dumptools + if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) { + // Check that we have access to MongoDump var cmd = buildMongoDumpCommand(); cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"'; const child_process = require('child_process'); child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) { - try { - if ((error != null) && (error != '')) { - if (parent.platform == 'win32') { - func(1, "Unable to find mongodump.exe, MongoDB database auto-backup will not be performed."); - } else { - func(1, "Unable to find mongodump, MongoDB database auto-backup will not be performed."); - } - } else { - func(); - } - } catch (ex) { console.log(ex); } + if ((error != null) && (error != '')) { + func(1, "Unable to find mongodump tool, backup will not be performed. Command tried: " + cmd); + return; + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} }); - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // Check that we have access to mysqldump - var backupPath = parent.backuppath; - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; } - try { parent.fs.mkdirSync(backupPath); } catch (e) { } - if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; } - var cmd = buildSqlDumpCommand(); cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"'); const child_process = require('child_process'); - child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) { - try { - if ((error != null) && (error != '')) { - if (parent.platform == 'win32') { - func(1, "Unable to find mysqldump.exe, MySQL/MariaDB database auto-backup will not be performed."); - } else { - func(1, "Unable to find mysqldump, MySQL/MariaDB database auto-backup will not be performed."); - } - } else { - func(); - } - } catch (ex) { console.log(ex); } + child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) { + if ((error != null) && (error != '')) { + func(1, "Unable to find mysqldump tool, backup will not be performed. Command tried: " + cmd); + return; + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} + }); + } else if (obj.databaseType == DB_POSTGRESQL) { + // Check that we have access to pg_dump + parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump'); + let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"' + + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password + + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName + + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"'); + const child_process = require('child_process'); + child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) { + if ((error != null) && (error != '')) { + func(1, "Unable to find pg_dump tool, backup will not be performed. Command tried: " + cmd); + return; + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} + }); } else { - func(); - } + //all ok, enable backup + parent.config.settings.autobackup.backupintervalhours = backupInterval;} } // MongoDB pending bulk read operation, perform fast bulk document reads. @@ -2913,157 +3522,268 @@ module.exports.CreateDB = function (parent, func) { } // Perform a server backup - obj.performingBackup = false; obj.performBackup = function (func) { + parent.debug('backup','Entering performBackup'); try { - if (obj.performingBackup) return 1; + if (obj.performingBackup) return 'Backup alreay in progress.'; + if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }}; obj.performingBackup = true; - //console.log('Performing backup...'); + let backupPath = parent.backuppath; + let dataPath = parent.datapath; - var backupPath = parent.backuppath; - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; } - try { parent.fs.mkdirSync(backupPath); } catch (e) { } - const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral'; - const dburl = parent.args.mongodb; const currentDate = new Date(); const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2); - const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix; - const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile); + obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip'); + parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile); - if ((obj.databaseType == 2) || (obj.databaseType == 3)) { - // Perform a MongoDump backup - const newBackupFile = 'mongodump-' + fileSuffix; - var newBackupPath = parent.path.join(backupPath, newBackupFile); + if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) { + // Perform a MongoDump + const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral'; + const dburl = parent.args.mongodb; + + obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive')); var cmd = buildMongoDumpCommand(); - cmd += (dburl) ? ' --archive=\"' + newBackupPath + '.archive\"' : - ' --db=\"' + dbname + '\" --archive=\"' + newBackupPath + '.archive\"'; - + cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' : + ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"'; + parent.debug('backup','Mongodump cmd: ' + cmd); const child_process = require('child_process'); - var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) { - try { - var mongoDumpSuccess = true; - backupProcess = null; - if ((error != null) && (error != '')) { mongoDumpSuccess = false; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); } + const dumpProcess = child_process.exec( + cmd, + { cwd: parent.parentpath }, + (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}} + ); + + dumpProcess.on('exit', (code) => { + if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;} + obj.createBackupfile(func); + }); - // Perform archive compression - var archiver = require('archiver'); - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip'); - var archive = null; - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) { - try { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); } catch (ex) { } - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword }); - } else { - archive = archiver('zip', { zlib: { level: 9 } }); - } - output.on('close', function () { - obj.performingBackup = false; - if (func) { if (mongoDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without mongodb database: ' + error); } } - obj.performCloudBackup(newAutoBackupPath + '.zip', func); - setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.archive', function () { }); } catch (ex) { console.log(ex); } }, 5000); - }); - output.on('end', function () { }); - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } }); - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.pipe(output); - if (mongoDumpSuccess == true) { archive.file(newBackupPath + '.archive', { name: newBackupFile + '.archive' }); } - archive.directory(parent.datapath, 'meshcentral-data'); - archive.finalize(); - } catch (ex) { console.log(ex); } - }); - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // Perform a MySqlDump backup const newBackupFile = 'mysqldump-' + fileSuffix; - var newBackupPath = parent.path.join(backupPath, newBackupFile); + obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql'); var cmd = buildSqlDumpCommand(); - cmd += ' --result-file=\"' + newBackupPath + '.sql\"'; - const child_process = require('child_process'); - var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) { - try { - var sqlDumpSuccess = true; - backupProcess = null; - if ((error != null) && (error != '')) { sqlDumpSuccess = false; console.log('ERROR: Unable to perform MySQL/MariaDB backup: ' + error + '\r\n'); } + cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"'; + parent.debug('backup','Maria/MySQLdump cmd: ' + cmd); - var archiver = require('archiver'); - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip'); - var archive = null; - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) { - try { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); } catch (ex) { } - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword }); - } else { - archive = archiver('zip', { zlib: { level: 9 } }); - } - output.on('close', function () { - obj.performingBackup = false; - if (func) { if (sqlDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without MySQL/MariaDB database: ' + error); } } - obj.performCloudBackup(newAutoBackupPath + '.zip', func); - setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.sql', function () { }); } catch (ex) { console.log(ex); } }, 5000); - }); - output.on('end', function () { }); - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } }); - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.pipe(output); - if (sqlDumpSuccess == true) { archive.file(newBackupPath + '.sql', { name: newBackupFile + '.sql' }); } - archive.directory(parent.datapath, 'meshcentral-data'); - archive.finalize(); - } catch (ex) { console.log(ex); } + const child_process = require('child_process'); + const dumpProcess = child_process.exec( + cmd, + { cwd: parent.parentpath }, + (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}} + ); + dumpProcess.on('exit', (code) => { + if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;} + obj.createBackupfile(func); + }); + + } else if (obj.databaseType == DB_SQLITE) { + //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files + obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3'); + // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html + parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile); + obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) { + if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;}; + //always finish/clean up + obj.createBackupfile(func); + }); + } else if (obj.databaseType == DB_POSTGRESQL) { + // Perform a PostgresDump backup + const newBackupFile = databaseName + '-pgdump-' + fileSuffix + '.sql'; + obj.newDBDumpFile = path.join(backupPath, newBackupFile); + let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"' + + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password + + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName + + " --file=" + obj.newDBDumpFile; + parent.debug('backup','Postgresqldump cmd: ' + cmd); + const child_process = require('child_process'); + const dumpProcess = child_process.exec( + cmd, + { cwd: dataPath }, + (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}} + ); + dumpProcess.on('exit', (code) => { + if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;} + obj.createBackupfile(func); }); } else { - // Perform a NeDB backup - var archiver = require('archiver'); - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip'); - var archive = null; - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) { - try { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); } catch (ex) { } - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword }); - } else { - archive = archiver('zip', { zlib: { level: 9 } }); - } - output.on('close', function () { obj.performingBackup = false; if (func) { func('Auto-backup completed.'); } obj.performCloudBackup(newAutoBackupPath + '.zip', func); }); - output.on('end', function () { }); - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } }); - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } }); - archive.pipe(output); - archive.directory(parent.datapath, 'meshcentral-data'); - archive.finalize(); + // NeDB/Acebase backup, no db dump needed, just make a file backup + obj.createBackupfile(func); } + } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); }; + return 'Starting auto-backup...'; + }; - // Remove old backups - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) { - var cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup); - parent.fs.readdir(parent.backuppath, function (err, dir) { - try { - if ((err == null) && (dir.length > 0)) { + obj.createBackupfile = function(func) { + parent.debug('backup', 'Entering createBackupfile'); + let archiver = require('archiver'); + let archive = null; + let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9); + + //if password defined, create encrypted zip + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) { + try { + //Only register format once, otherwise it triggers an error + if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); } + archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword }); + if (func) { func('Creating encrypted ZIP'); } + } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups + obj.backupStatus |= BACKUPFAIL_ZIPMODULE; + if (func) { func('Zipencryptionmodule failed, aborting');} + console.error('Zipencryptionmodule failed, aborting'); + } + } else { + if (func) { func('Creating a NON-ENCRYPTED ZIP'); } + archive = archiver('zip', { zlib: { level: zipLevel } }); + } + + //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP) + if (obj.backupStatus == 0) { + // Zip the data directory with the dbdump|NeDB files + let output = fs.createWriteStream(obj.newAutoBackupFile); + + // Archive finalized and closed + output.on('close', function () { + if (obj.backupStatus == 0) { + let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb"; + console.log(mesg); + if (func) { func(mesg); }; + obj.performCloudBackup(obj.newAutoBackupFile, func); + obj.removeExpiredBackupfiles(func); + + } else { + let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile; + if (func) { func(mesg) } + else { parent.addServerWarning(mesg, true ) }; + if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { console.error('Failed to clean up backupfile: ' + err.message) }) }; + }; + if (obj.databaseType != DB_NEDB) { + //remove dump archive file, because zipped and otherwise fills up + if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) }; + }; + obj.performingBackup = false; + obj.backupStatus = 0x0; + } + ); + output.on('end', function () { }); + output.on('error', function (err) { + if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) { + console.error('Output error: ' + err.message); + if (func) { func('Output error: ' + err.message); }; + obj.backupStatus |= BACKUPFAIL_ZIPCREATE; + archive.abort(); + }; + }); + archive.on('warning', function (err) { + //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files) + //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume + //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends' + if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) { + console.log('Zip warning: ' + err.message); + if (func) { func('Zip warning: ' + err.message); }; + obj.backupStatus |= BACKUPFAIL_ZIPCREATE; + archive.abort(); + }; + }); + archive.on('error', function (err) { + if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) { + console.error('Zip error: ' + err.message); + if (func) { func('Zip error: ' + err.message); }; + obj.backupStatus |= BACKUPFAIL_ZIPCREATE; + archive.abort(); + } + }); + archive.pipe(output); + + let globIgnoreFiles; + //slice in case exclusion gets pushed + globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : []; + if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm + //archiver.glob doesn't seem to use the third param, archivesubdir. Bug? + //workaround: go up a dir and add data dir explicitly to keep the zip tidy + archive.glob((datapathFoldername + '/**'), { + cwd: datapathParentPath, + ignore: globIgnoreFiles, + skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : []) + }); + + if (parent.config.settings.autobackup.backupwebfolders) { + if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); } + if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); } + if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); } + }; + if (parent.config.settings.autobackup.backupotherfolders) { + archive.directory(parent.filespath, 'meshcentral-files'); + archive.directory(parent.recordpath, 'meshcentral-recordings'); + }; + //add dbdump to the root of the zip + if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) }); + archive.finalize(); + } else { + //failed somewhere before zipping + console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')'); + if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') } + else { + parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true); + } + //Just in case something's there + if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); }; + obj.backupStatus = 0x0; + obj.performingBackup = false; + }; + }; + + // Remove expired backupfiles by filenamedate + obj.removeExpiredBackupfiles = function (func) { + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) { + let cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup); + fs.readdir(parent.backuppath, function (err, dir) { + try { + if (err == null) { + if (dir.length > 0) { + let fileName = parent.config.settings.autobackup.backupname; + let checked = 0; + let removed = 0; for (var i in dir) { var name = dir[i]; - if (name.startsWith('meshcentral-autobackup-') && name.endsWith('.zip')) { - var timex = name.substring(23, name.length - 4).split('-'); + parent.debug('backup', "checking file: ", path.join(parent.backuppath, name)); + if (name.startsWith(fileName) && name.endsWith('.zip')) { + var timex = name.substring(fileName.length, name.length - 4).split('-'); if (timex.length == 5) { + checked++; var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4])); - if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } } + if (fileDate && (cutoffDate > fileDate)) { + console.log("Removing expired backup file: ", path.join(parent.backuppath, name)); + fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } }); + removed++; + } } + else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); } } } - } - } catch (ex) { console.log(ex); } - }); - } - } catch (ex) { console.log(ex); } - return 0; + let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' }); + parent.debug (mesg); + if (func) { func(mesg); } + } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')} + } + else + { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); } + } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); } + }); + } } // Perform cloud backup obj.performCloudBackup = function (filename, func) { - // WebDAV Backup if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) { - const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; } + parent.debug( 'backup', 'Entering WebDAV backup'); + if (func) { func('Entering WebDAV backup.'); } + const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; } // Fetch the folder name var webdavfolderName = 'MeshCentral-Backups'; if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; } @@ -3071,21 +3791,28 @@ module.exports.CreateDB = function (parent, func) { // Clean up our WebDAV folder function performWebDavCleanup(client) { if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) { - var directoryItems = client.getDirectoryContents(webdavfolderName); + let fileName = parent.config.settings.autobackup.backupname; + //only files matching our backupfilename + let directoryItems = client.getDirectoryContents(webdavfolderName, { deep: false, glob: "/**/" + fileName + "*.zip" }); directoryItems.then( function (files) { for (var i in files) { files[i].xdate = new Date(files[i].lastmod); } files.sort(xdateTimeSort); + parent.debug('backup','WebDAV filtered directory contents: ' + JSON.stringify(files, null, 4)); while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) { - client.deleteFile(files.shift().filename).then(function (state) { - if (func) { func('WebDAV file deleted.'); } + let delFile = files.shift().filename; + client.deleteFile(delFile).then(function (state) { + parent.debug('backup','WebDAV file deleted: ' + delFile); + if (func) { func('WebDAV file deleted: ' + delFile); } }).catch(function (err) { - if (func) { func('WebDAV (deleteFile) error: ' + err); } + console.error(err); + if (func) { func('WebDAV (deleteFile) error: ' + err.message); } }); } } ).catch(function (err) { - if (func) { func('WebDAV (getDirectoryContents) error: ' + err); } + console.error(err); + if (func) { func('WebDAV (getDirectoryContents) error: ' + err.message); } }); } } @@ -3094,14 +3821,14 @@ module.exports.CreateDB = function (parent, func) { function performWebDavUpload(client, filepath) { require('fs').stat(filepath, function(err,stat){ var fileStream = require('fs').createReadStream(filepath); - fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } }) - fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } }) + fileStream.on('close', function () { console.log('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); if (func) { func('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); } }) + fileStream.on('error', function (err) { console.error(err); if (func) { func('WebDAV (fileUpload) error: ' + err.message); } }) fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath), { headers: { "Content-Length": stat.size } })); - if (func) { func('Uploading using WebDAV...'); } + parent.debug('backup', 'Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url); + if (func) { func('Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url); } }); } - if (func) { func('Attempting WebDAV upload...'); } const { createClient } = require('webdav'); const client = createClient(parent.config.settings.autobackup.webdav.url, { username: parent.config.settings.autobackup.webdav.username, @@ -3115,19 +3842,23 @@ module.exports.CreateDB = function (parent, func) { performWebDavUpload(client, filename); }else{ client.createDirectory(webdavfolderName, {recursive: true}).then(function (a) { - if (func) { func('WebDAV folder created'); } + console.log('backup','WebDAV folder created: ' + webdavfolderName); + if (func) { func('WebDAV folder created: ' + webdavfolderName); } performWebDavUpload(client, filename); }).catch(function (err) { - if (func) { func('WebDAV (createDirectory) error: ' + err); } + console.error(err); + if (func) { func('WebDAV (createDirectory) error: ' + err.message); } }); } }).catch(function (err) { - if (func) { func('WebDAV (exists) error: ' + err); } + console.error(err); + if (func) { func('WebDAV (exists) error: ' + err.message); } }); } // Google Drive Backup if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) { + parent.debug( 'backup', 'Entering Google Drive backup'); obj.Get('GoogleDriveBackup', function (err, docs) { if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return; if (func) { func('Attempting Google Drive upload...'); } @@ -3203,13 +3934,110 @@ module.exports.CreateDB = function (parent, func) { }); }); } + + // S3 Backup + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) { + parent.debug( 'backup', 'Entering S3 backup'); + var s3folderName = 'MeshCentral-Backups'; + if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; } + // Construct the config object + var accessKey = parent.config.settings.autobackup.s3.accesskey, + secretKey = parent.config.settings.autobackup.s3.secretkey, + endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com', + port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443, + useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true, + bucketName = parent.config.settings.autobackup.s3.bucketname, + pathPrefix = s3folderName, + threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0, + fileToUpload = filename; + // Create a MinIO client + const Minio = require('minio'); + var minioClient = new Minio.Client({ + endPoint: endpoint, + port: port, + useSSL: useSsl, + accessKey: accessKey, + secretKey: secretKey + }); + // List objects in the specified bucket and path prefix + var listObjectsPromise = new Promise(function(resolve, reject) { + var items = []; + var stream = minioClient.listObjects(bucketName, pathPrefix, true); + stream.on('data', function(item) { + if (!item.name.endsWith('/')) { // Exclude directories + items.push(item); + } + }); + stream.on('end', function() { + resolve(items); + }); + stream.on('error', function(err) { + reject(err); + }); + }); + listObjectsPromise.then(function(objects) { + // Count the number of files + var fileCount = objects.length; + // Return if no files to carry on uploading + if (fileCount === 0) { return Promise.resolve(); } + // Sort the files by LastModified date (oldest first) + objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); }); + // Check if the threshold is zero and return if + if (threshold === 0) { return Promise.resolve(); } + // Check if the number of files exceeds the threshold (maxfiles) is 0 + if (fileCount >= threshold) { + // Calculate how many files need to be deleted to make space for the new file + var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file + if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); } + // Create an array of promises for deleting files + var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) { + return new Promise(function(resolve, reject) { + minioClient.removeObject(bucketName, fileToDelete.name, function(err) { + if (err) { + reject(err); + } else { + if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); } + resolve(); + } + }); + }); + }); + // Wait for all deletions to complete + return Promise.all(deletePromises); + } else { + return Promise.resolve(); // No deletion needed + } + }).then(function() { + // Determine the upload path by combining the pathPrefix with the filename + var fileName = require('path').basename(fileToUpload); + var uploadPath = require('path').join(pathPrefix, fileName); + // Upload a new file + var uploadPromise = new Promise(function(resolve, reject) { + if (func) { func('Uploading file ' + uploadPath + ' to S3'); } + minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) { + if (err) { + reject(err); + } else { + if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); } + resolve(etag); + } + }); + }); + return uploadPromise; + }).catch(function(error) { + if (func) { func('Error managing files in S3: ' + error); } + }); + } } // Transfer NeDB data into the current database obj.nedbtodb = function (func) { var nedbDatastore = null; - try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies. - if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it. + try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support. + if (nedbDatastore == null) { + try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies. + if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it. + } var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true }; @@ -3303,6 +4131,7 @@ module.exports.CreateDB = function (parent, func) { // Called when a node has changed function dbNodeChange(nodeChange, added) { + if (parent.webserver == null) return; common.unEscapeLinksFieldName(nodeChange.fullDocument); const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0]; parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 }); @@ -3326,12 +4155,13 @@ module.exports.CreateDB = function (parent, func) { } // Send the mesh update - if (mesh.deleted) { mesh.action = 'deletemesh'; } else { mesh.action = (added ? 'createmesh' : 'meshchange'); } - mesh.meshid = mesh._id; - mesh.nolog = 1; - delete mesh.type; - delete mesh._id; - parent.DispatchEvent(['*', mesh.meshid], obj, parent.webserver.CloneSafeMesh(mesh)); + var mesh2 = Object.assign({}, mesh); // Shallow clone + if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); } + mesh2.meshid = mesh2._id; + mesh2.nolog = 1; + delete mesh2.type; + delete mesh2._id; + parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2)); } // Called when a user account has changed @@ -3375,17 +4205,18 @@ module.exports.CreateDB = function (parent, func) { } // Send the user group update - usergroup.action = (added ? 'createusergroup' : 'usergroupchange'); - usergroup.ugrpid = usergroup._id; - usergroup.nolog = 1; - delete usergroup.type; - delete usergroup._id; - parent.DispatchEvent(['*', usergroup.ugrpid], obj, usergroup); + var usergroup2 = Object.assign({}, usergroup); // Shallow clone + usergroup2.action = (added ? 'createusergroup' : 'usergroupchange'); + usergroup2.ugrpid = usergroup2._id; + usergroup2.nolog = 1; + delete usergroup2.type; + delete usergroup2._id; + parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2); } function dbMergeSqlArray(arr) { var x = ''; - for (var i in arr) { if (x != '') { x += ','; } x += '"' + arr[i] + '"'; } + for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; } return x; } diff --git a/dependencies.txt b/dependencies.txt index 41ec5dec..6bf8bcdd 100644 --- a/dependencies.txt +++ b/dependencies.txt @@ -1,16 +1,16 @@ - "archiver": "5.3.2", - "body-parser": "1.20.2", + "@seald-io/nedb": "4.0.4", + "archiver": "7.0.1", + "body-parser": "1.20.3", "cbor": "5.2.0", - "compression": "1.7.4", - "cookie-session": "2.0.0", - "express": "4.18.2", - "express-handlebars": "5.3.5", - "express-ws": "4.0.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", - "@yetzt/nedb": "1.8.0", "node-forge": "1.3.1", - "ua-parser-js": "1.0.37", - "ws": "8.14.2", + "ua-parser-js": "1.0.39", + "ws": "8.18.0", "yauzl": "2.10.0" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 31d5139d..26806e1d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 COPY ./ /opt/meshcentral/meshcentral/ @@ -18,7 +18,7 @@ RUN if ! [ -z "$DISABLE_TRANSLATE" ] && [ "$DISABLE_TRANSLATE" != "yes" ] && [ " fi # 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 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 -FROM --platform=$TARGETPLATFORM alpine:3.19 +FROM --platform=$TARGETPLATFORM alpine:3.21 #Add non-root user, add installation directories and assign proper permissions RUN mkdir -p /opt/meshcentral/meshcentral @@ -62,8 +62,8 @@ ENV MONGO_URL="" ENV HOSTNAME="localhost" ENV ALLOW_NEW_ACCOUNTS="true" ENV ALLOWPLUGINS="false" -ENV LOCALSESSIONRECORDING="false" -ENV MINIFY="true" +ENV LOCALSESSIONRECORDING="true" +ENV MINIFY="false" ENV WEBRTC="false" ENV IFRAME="false" ENV SESSION_KEY="" @@ -83,12 +83,12 @@ COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral COPY ./docker/startup.sh ./startup.sh COPY ./docker/config.json.template /opt/meshcentral/config.json.template -# install dependencies from package.json and nedb -RUN cd meshcentral && npm install && npm install nedb +# install dependencies from package.json +RUN cd meshcentral && npm install # 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 "$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 diff --git a/docker/config.json.template b/docker/config.json.template index a6fe3201..cef4ad33 100644 --- a/docker/config.json.template +++ b/docker/config.json.template @@ -21,9 +21,9 @@ "": { "_title": "MyServer", "_title2": "Servername", - "minify": true, + "minify": false, "NewAccounts": true, - "localSessionRecording": false, + "localSessionRecording": true, "_userNameIsEmail": true, "_certUrl": "my.reverse.proxy" } diff --git a/docker/startup.sh b/docker/startup.sh index c198d847..da3f0b34 100644 --- a/docker/startup.sh +++ b/docker/startup.sh @@ -18,7 +18,7 @@ else 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/\"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/\"AllowFraming\": false/\"AllowFraming\": $IFRAME/" meshcentral-data/"${CONFIG_FILE}" if [ -z "$SESSION_KEY" ]; then diff --git a/docs/docs/how-to-contribute/images/translation-msg-output.png b/docs/docs/how-to-contribute/images/translation-msg-output.png new file mode 100644 index 00000000..ab94a484 Binary files /dev/null and b/docs/docs/how-to-contribute/images/translation-msg-output.png differ diff --git a/docs/docs/how-to-contribute/index.md b/docs/docs/how-to-contribute/index.md new file mode 100644 index 00000000..ed3d87aa --- /dev/null +++ b/docs/docs/how-to-contribute/index.md @@ -0,0 +1,56 @@ +# Contribute to MeshCentral + +## Contributing to MeshCentral via GitHub Pull Request + +If you're looking to contribute beyond translations, such as updating documentation or enhancing the software by adding features or fixing bugs, the process involves several key steps: + +1. **Fork the Repository:** Start by forking the [MeshCentral](https://github.com/Ylianst/MeshCentral) repository on GitHub. This creates a copy of the repository under your own GitHub account, allowing you to make changes without affecting the original project. + +2. **Make Your Changes** + - In your forked repository, create a new branch to keep your changes organized. This helps in managing different contributions separately. + - Make the necessary changes in your repository. This could involve updating documentation files or modifying code to add new features or fix bugs. + +3. **Review Your Changes:** Before submitting your work, carefully review the changes you’ve made. Check the "Files Changed" section on GitHub to ensure that all modifications are intended and correctly implemented. + +4. **Submit a Pull Request** + - Once your changes are ready and reviewed, submit a pull request (PR) from your branch to the `master` branch of the main MeshCentral repository. + - When creating the pull request, provide a clear and detailed description of what changes have been made and why. This helps maintainers understand the purpose of your contributions. + +5. **Wait for Review:** After submitting your pull request, wait for a project maintainer to review your contribution. Review time can vary depending on the complexity of the changes and the availability of the maintainers. + +6. **Respond to Feedback:** The maintainer may request further modifications or provide feedback on your pull request. Be prepared to make additional changes based on their suggestions to ensure that your contribution meets the project’s standards and requirements. + +7. **Final Steps:** Once your pull request is approved and merged by a maintainer, your contributions will be incorporated into the MeshCentral project. Congratulations, and thank you for helping improve MeshCentral! + +--- + +## Contribute to MeshCentral's Multilingual Support + +To make MeshCentral multilingual, your contributions are crucial. Follow these steps to translate the interface into various languages. + +1. **Remove Local Translations:** Delete `translate.json` from your `meshcentral-data` folder. This file contains your local copy of translations, which may become outdated as new features and texts are added. + +2. **Access MeshCentral:** Ensure you are logged into MeshCentral. +3. **Open Translation Tool:** Visit `https://YOURMESHCENTRALSERVER.COM/translator.htm` to access the translation interface. +4. **Choose a Language:** Select the language you wish to translate from the list provided. + +5. **Translate Text:** Use the search function or scroll through the list to find text segments you want to translate. Utilize the "show no translations only" checkbox to filter untranslated texts. +6. **Enter Translations:** For each text segment, enter your translation in the bottom box (not the top one) and click `SET (F1)`. +7. **Repeat Translation:** Continue translating by repeating steps 5 and 6 for other texts as desired. + +8. **Save and Apply Translations** + - Click `SAVE TO SERVER (F3)` to save your translations to `meshcentral-data/translate.json` locally in your MeshCentral server. + - Optionally, click `SAVE TO FILE (F4)` to download the `translate.json` file for offline review or sharing. + +9. **Deploy Translations:** Click `TRANSLATE SERVER` and allow some time for the process to complete (approximately 5-15 minutes depending on server specifications). This command line output will indicate when the translation is complete. +![](images/translation-msg-output.png) + +10. **Finalize Changes:** It’s crucial to restart MeshCentral to ensure that the translated files are picked up correctly. +11. **Share your translations:** Once a language translation is complete, take the latest `translation.json` and share it by emailing it to the maintainer (Ylianst, `ylianst@gmail.com`) or by submitting it to the MeshCentral GitHub repository via a pull request. + +--- + +#### Additional Information: + - If you make any changes to `default.handlebars`, run the translate server to propagate these modifications to the language-specific handlebar files located in `node_modules/meshcentral/views/translations`. + +By following these steps, you help MeshCentral support any language you choose, making it more accessible worldwide. By sharing your translations with us, you also help make these languages available to other users, improving the community and extending the software's reach. diff --git a/docs/docs/index.md b/docs/docs/index.md index a5ce3a30..fabdc903 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -12,7 +12,7 @@ For more information, [visit MeshCentral.com](https://www.meshcentral.com/). [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/) diff --git a/docs/docs/install/index.md b/docs/docs/install/index.md index 4498d74a..988cf099 100644 --- a/docs/docs/install/index.md +++ b/docs/docs/install/index.md @@ -23,7 +23,7 @@ You can run the MeshCentral Server with --help to get options for background ins Once you get MeshCentral installed, the first user account that is created will be the server administrator. So, don't delay and navigate to the login page and create a new account. You can then start using your server right away. A lot of the fun with MeshCentral is the 100's of configuration options that are available in the config.json file. You can put your own branding on the web pages, setup a SMTP email server, SMS services and much more. -You can look [here for simple config.json](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config.json), [here for a more advanced configuration](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config-advanced.json) and [here for all possible configuration options](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json). You can also take a look at the [MeshCentral User's Guide](https://meshcentral.com/docs/MeshCentral2InstallGuide.pdf) and [tutorial videos](https://www.youtube.com/@MeshCentral/videos) for additional help. +You can look [here for simple config.json](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config.json), [here for a more advanced configuration](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config-advanced.json) and [here for all possible configuration options](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json). You can also take a look at the [tutorial videos](https://www.youtube.com/@MeshCentral/videos) for additional help. ## Video Walkthru diff --git a/docs/docs/install/install2.md b/docs/docs/install/install2.md index 826fc576..39312467 100644 --- a/docs/docs/install/install2.md +++ b/docs/docs/install/install2.md @@ -4,6 +4,45 @@ This guide is specifically intended to help users install MeshCentral from start to finish. Once installed, you can take a look at the MeshCentral user’s guide for information on how to configure MeshCentral for your specific use. In this document, we will look at installing MeshCentral on AWS Linux, Raspberry Pi and Ubuntu. +## Docker + + + +``` +docker pull ghcr.io/ylianst/meshcentral:master +``` + +!!!warning + Do not use the built in mesh update function. Update docker the docker way. + +### Docker Compose + +``` +version: '3' +services: + meshcentral: + restart: unless-stopped # always restart the container unless you stop it + image: ghcr.io/ylianst/meshcentral:1.1.27 # 1.1.27 is a version number OR use master for the master branch of bug fixes + ports: + - 80:80 # HTTP + - 443:443 # HTTPS + - 4433:4433 # AMT (Optional) + volumes: + - data:/opt/meshcentral/meshcentral-data # config.json and other important files live here + - user_files:/opt/meshcentral/meshcentral-files # where file uploads for users live + - backup:/opt/meshcentral/meshcentral-backups # location for the meshcentral backups - this should be mounted to an external storage + - web:/opt/meshcentral/meshcentral-web # location for site customization files +volumes: + data: + driver: local + user_files: + driver: local + backup: + driver: local + web: + driver: local +``` + ## Quick Start For some who want to skip this document entirely, there are quick install scripts that will get a MeshCentral2 instance up and running on Linux in a few minutes. These scripts will pretty much do what this document explains very rapidly. Right now, there are two such scripts available: @@ -40,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. +### 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. + +[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/meshcentral) + ## 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. @@ -884,7 +929,7 @@ The last line will run MeshCentral manually and allow it to install any missing ``` sudo chown -R meshcentral:meshcentral /opt/meshcentral -sudo chmod 755 –R /opt/meshcentral/meshcentral-* +sudo chmod -R 755 /opt/meshcentral/meshcentral-* ``` To make this work, you will need to make MeshCentral work with MongoDB because the /meshcentral-data folder will be read-only. In addition, MeshCentral will not be able to update itself since the account does not have write access to the /node_modules files, so the update will have to be manual. First used systemctl to stop the MeshCentral server process, than use this: @@ -901,7 +946,7 @@ This will perform the update to the latest server on NPM and re-set the permissi MeshCentral allows users to upload and download files stores in the server’s `meshcentral-files` folder. In an increased security setup, we still want the server to be able to read and write files to this folder and we can allow this with: ``` -sudo chmod 755 –R /opt/meshcentral/meshcentral-files +sudo chmod -R 755 /opt/meshcentral/meshcentral-files ``` If you plan on using the increased security installation along with MeshCentral built-in Let’s Encrypt support you will need to type the following commands to make the `letsencrypt` folder in `meshcentral-data` writable. @@ -909,7 +954,7 @@ If you plan on using the increased security installation along with MeshCentral ``` sudo mkdir /opt/meshcentral/meshcentral-data sudo mkdir /opt/meshcentral/meshcentral-data/letsencrypt -sudo chmod 755 –R /opt/meshcentral/meshcentral-data/letsencrypt +sudo chmod -R 755 /opt/meshcentral/meshcentral-data/letsencrypt ``` This will allow the server to get and periodically update its Let’s Encrypt certificate. If this is not done, the server will generate an `ACCES: permission denied` exception. diff --git a/docs/docs/meshcentral/agents.md b/docs/docs/meshcentral/agents.md index d17e11be..6cba2f14 100644 --- a/docs/docs/meshcentral/agents.md +++ b/docs/docs/meshcentral/agents.md @@ -352,3 +352,11 @@ wpfhwacceleration (ON|OFF|STATUS) ``` zip (output file name), input1 [, input n] ``` + +## Agent msh options + +You can find a full list of options for the agent [here](https://github.com/Ylianst/MeshAgent?tab=readme-ov-file#msh-format) + +`skipmaccheck=1`: Will not regenerate the agents nodeid and cause duplication of the agent when the MAC address changes. + +You can add options to your .msh on agent install with [this](https://github.com/Ylianst/MeshCentral/blob/15ff7d12a1e4e5d78936b473ea207b7e02b8ff26/meshcentral-config-schema.json#L2504) diff --git a/docs/docs/meshcentral/assistant.md b/docs/docs/meshcentral/assistant.md index 5da427ef..0462df3e 100644 --- a/docs/docs/meshcentral/assistant.md +++ b/docs/docs/meshcentral/assistant.md @@ -14,16 +14,28 @@ ![agent invite code](images/assistant_agent_code.png) -## Agent Invitation Link - -For web page customization: +## Agent Invitation +Click on the 'Invite' button next to the device group name to access it. +### Link Invitation +For link invitation web page customization: 1. Alongside `meshcentral-data` create a folder called `meshcentral-web` 2. Create a `views` folder in it and copy the file `node_modules/meshcentral/views/invite.handlebars` into it. -3. That copy will be served instead of the default one, you can customize as you want. +3. That copy will be served instead of the default one, so you can customize it as you want. ![agent invite code](images/assistant_invitation_link.png) +### Email Invitation +This option will show up if you have an SMTP email server set up with MeshCentral. + +For invitation email customization: + +1. Alongside `meshcentral-data` create a folder called `meshcentral-web` +2. Create an `emails` folder in it and copy the files `node_modules/meshcentral/emails/mesh-invite.txt` and `node_modules/meshcentral/emails/mesh-invite.html` into it. +3. These copies will be used instead of the default ones, so you can customize them as you want. + +![email-invitation](images/email-invitation.png) + ## Email notification You can also get an email notification when someone clicks the "Request Help" button in the Assistant agent. diff --git a/docs/docs/meshcentral/codesigning.md b/docs/docs/meshcentral/codesigning.md index 8fd46f7b..25e7a37a 100644 --- a/docs/docs/meshcentral/codesigning.md +++ b/docs/docs/meshcentral/codesigning.md @@ -73,7 +73,7 @@ When doing sign/unsign, you can also change resource properties of the generated ## Automatic Agent Code Signing -If you want to self-sign the mesh agent so you can whitelist the software in your AV, and lock it to your server and organization. +If you want to self-sign the mesh agent so you can whitelist the software in your AV, as well as lock it to your server and organization:
diff --git a/docs/docs/meshcentral/customization.md b/docs/docs/meshcentral/customization.md index 7050befa..654bb0bf 100644 --- a/docs/docs/meshcentral/customization.md +++ b/docs/docs/meshcentral/customization.md @@ -1,6 +1,6 @@ # Customization -Whitelabeling your MeshCentral installation to personalize it to your companies brand, as well as having your own terms of use is one of the first things many people do after installation. +Whitelabeling your MeshCentral installation to personalize it to your company's brand, as well as having your own terms of use is one of the first things many people do after installation.
@@ -8,7 +8,7 @@ Whitelabeling your MeshCentral installation to personalize it to your companies ## Web Branding -You can put you own logo on the top of the web page. To get started, get the file “logoback.png” from the folder “node_modules/meshcentral/public/images” and copy it to your “meshcentral-data” folder. In this example, we will change the name of the file “logoback.png” to “title-mycompany.png”. Then use any image editor to change the image and place your logo. +You can put your own logo on the top of the web page. To get started, get the file “logoback.png” from the folder “node_modules/meshcentral/public/images” and copy it to your “meshcentral-data” folder. In this example, we will change the name of the file “logoback.png” to “title-mycompany.png”. Then use any image editor to change the image and place your logo. ![](images/2022-05-19-00-38-51.png) @@ -33,7 +33,7 @@ Once done, edit the config.json file and set one or all of the following values: }, ``` -This will set the title and sub-title text to empty and set the background image to the new title picture file. You can now restart the serve and take a look at the web page. Both the desktop and mobile sites will change. +This will set the title and sub-title text to empty and set the background image to the new title picture file. You can now restart the server and take a look at the web page. Both the desktop and mobile sites will change. ![](images/2022-05-19-00-39-35.png) @@ -51,6 +51,17 @@ If, for example, MeshCentral is running on a Raspberry Pi. You may want to put a This is great to personalize the look of the server within the web site. +### Customizing Web Icons +MeshCentral lets you change the icons for different devices shown in the Web User Interface. To do this the proper way, you should make a new folder called `meshcentral-web` in the main directory, where you find other folders like `meshcentral-data`, `meshcentral-backup`, `meshcentral-files`, and `node-modules`. Inside `meshcentral-web`, make another folder named `public` and copy the entire `node_modules/meshcentral/public/images` folder into this new `meshcentral-web/public` folder and then edit the files in `meshcentral-web/public/images/`. This step is suggested because if MeshCentral updates, it might delete any changes in `node_modules`. But, changes in `meshcentral-web` will stay safe, and MeshCentral will use these files instead of the originals in `node_modules`. + +To update device icons, you need to edit these files: `meshcentral-web/public/images/webp/iconsXX.webp` (`icons16.webp`, `icons32.webp`, `icons50.webp`, `icons100.webp`), and `meshcentral-web/public/images/iconsXX.png` (`icons16.png`, `icons32.png`, `icons50.png`, `icons64.png`, `icons100.png`) and the corresponding `meshcentral-web/public/images/icons256-X-1.png`. Make sure to keep the resolution of these files as it is. + +By following these steps, you can customize any icon in MeshCentral. Just find and change the corresponding image files in the `meshcentral-web/public/images` folder. Similarly, you can also move other folders from `node_modules/meshcentral` to `meshcentral-web` while keeping the original folder structure. This allows you to modify other parts of MeshCentral too, like the `.handlebars` templates for the web interface. Simply copy files from `node_modules/meshcentral/views` to `meshcentral-web/views` and make your changes in `meshcentral-web`. This lets you match MeshCentral's look to your company's brand or your own style. +![](images/custom-web-icons.png) + +### Customizing Agent Invitation +Agents can be invited by public link or via email. [Click Here](assistant.md#agent-invitation) to see details. + ## Agent Branding You can customize the Agent to add your own logo, change the title bar, install text, the service name, or even colors! diff --git a/docs/docs/meshcentral/debugging.md b/docs/docs/meshcentral/debugging.md index 339e4f53..68a3725e 100644 --- a/docs/docs/meshcentral/debugging.md +++ b/docs/docs/meshcentral/debugging.md @@ -23,7 +23,7 @@ Make sure you understand how MeshCentral works with your browser using chrome de ### Understanding node and paths -Note that when running MeshCentral, you should always run like from the path that is parent to node_modules, so you do this: +Note that when running MeshCentral, you should always run from the path that is parent to node_modules, so you do this: ``` cd C:\Program Files\Open Source\MeshCentral @@ -39,7 +39,7 @@ node meshcentral The problem with the second command is that NPM may install missing modules in the incorrect location. -Also, in general I recommend not using the MeshCentral MSI Installer and just install manually unless you are very much scared of the command prompt. Anyone that knows about bit about the shell should install MeshCentral like this: +Also, in general I recommend not using the MeshCentral MSI Installer and just install manually unless you are very scared of the command prompt. Anyone that knows a bit about the shell should install MeshCentral like this: ``` mkdir c:\meshcentral @@ -50,11 +50,11 @@ node node_modules\meshcentral node node_modules\meshcentral --install ``` -This way, just have a lot more control over what is going on. Just my opinion, the MSI installer basically does the same thing and installs NodeJS for you. +This way, you have a lot more control over what is going on. In my opinion, the MSI installer basically does the same thing and installs NodeJS for you. ### Unable to update server -Generally the problem is that MeshCentral can't find the npm tool and so, can't run it to see if there is a new version. You can fix this by setting the path to npm in the config.json like this: +Generally the problem is that MeshCentral can't find the npm tool and therefore, can't run it to see if there is a new version. You can fix this by setting the path to npm in the config.json like this: ```json { @@ -86,14 +86,59 @@ node node_modules/meshcentral --stop ### Port Troubleshooting on server -If you're getting a `port 4433 is not available` error, this is because someone else is using this port, very likely another instance of MeshCentral. If your MeshCentral server is bound to ports 81/444 MeshCentral could not get port 80/443 and got the next available ones. +If you're getting a `port 4433 is not available` error, this is because another process is using this port, very likely another instance of MeshCentral. If your MeshCentral server is bound to ports 81/444 MeshCentral could not get port 80/443 and got the next available ones. 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 -`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 diff --git a/docs/docs/meshcentral/faq.md b/docs/docs/meshcentral/faq.md index 815adab5..1c5b2404 100644 --- a/docs/docs/meshcentral/faq.md +++ b/docs/docs/meshcentral/faq.md @@ -67,17 +67,17 @@ To be able to control keyboard and mouse ![](images/2023-11-29-12-58-36.png) -## I'm using cloudflare and I'm getting a black screen but the mouse moves? +## I'm using CloudFlare and I'm getting a black screen but the mouse moves? -If you are using cloudflare for your DNS hosting and your remote screen is black, DONT PANIC! +If you are using CloudFlare for your DNS hosting and your remote screen is black, DONT PANIC! -Unfortunately, MeshCentral currently doesnt work with CloudFlares Proxy DNS Mode, so the remote agent devices have to connect directly to your MeshCentral Server instead of being proxied! +Unfortunately, MeshCentral doesn't always work with CloudFlare's Proxy DNS Mode. -The fix is to simply disable the 'Proxy Status' to OFF inside your DNS A Record. +The fix is to simply set the 'Proxy Status' to OFF inside your DNS A Record, within the CloudFlare control panel. -Simple follow the steps [here](https://developers.cloudflare.com/fundamentals/setup/manage-domains/pause-cloudflare/#disable-proxy-on-dns-records) +Simply follow the steps [here](https://developers.cloudflare.com/fundamentals/setup/manage-domains/pause-cloudflare/#disable-proxy-on-dns-records) Once done, open your firewall for the `port` and `agentPort` ports of where your meshcentral is hosted, then restart your MeshCentral Server -The is currently a PINNED GitHub issue about this [here](https://github.com/Ylianst/MeshCentral/issues/5302) +There is currently a PINNED GitHub issue about this [here](https://github.com/Ylianst/MeshCentral/issues/5302) diff --git a/docs/docs/meshcentral/images/In-production.png b/docs/docs/meshcentral/images/In-production.png new file mode 100644 index 00000000..c1fc2e64 Binary files /dev/null and b/docs/docs/meshcentral/images/In-production.png differ diff --git a/docs/docs/meshcentral/images/OAuth-Internal-External.png b/docs/docs/meshcentral/images/OAuth-Internal-External.png new file mode 100644 index 00000000..13b14c8a Binary files /dev/null and b/docs/docs/meshcentral/images/OAuth-Internal-External.png differ diff --git a/docs/docs/meshcentral/images/custom-web-icons.png b/docs/docs/meshcentral/images/custom-web-icons.png new file mode 100644 index 00000000..9edb5569 Binary files /dev/null and b/docs/docs/meshcentral/images/custom-web-icons.png differ diff --git a/docs/docs/meshcentral/images/email-invitation.png b/docs/docs/meshcentral/images/email-invitation.png new file mode 100644 index 00000000..db9d2196 Binary files /dev/null and b/docs/docs/meshcentral/images/email-invitation.png differ diff --git a/docs/docs/meshcentral/images/gc-newproject.png b/docs/docs/meshcentral/images/gc-newproject.png new file mode 100644 index 00000000..715b68f9 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-newproject.png differ diff --git a/docs/docs/meshcentral/images/gc-oauthconsent.png b/docs/docs/meshcentral/images/gc-oauthconsent.png new file mode 100644 index 00000000..a493c589 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-oauthconsent.png differ diff --git a/docs/docs/meshcentral/images/gc-oauthconsent2.png b/docs/docs/meshcentral/images/gc-oauthconsent2.png new file mode 100644 index 00000000..10697dd1 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-oauthconsent2.png differ diff --git a/docs/docs/meshcentral/images/gc-oauthcredentials.png b/docs/docs/meshcentral/images/gc-oauthcredentials.png new file mode 100644 index 00000000..243928b0 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-oauthcredentials.png differ diff --git a/docs/docs/meshcentral/images/gc-oauthscopes.png b/docs/docs/meshcentral/images/gc-oauthscopes.png new file mode 100644 index 00000000..10697dd1 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-oauthscopes.png differ diff --git a/docs/docs/meshcentral/images/gc-playground.webp b/docs/docs/meshcentral/images/gc-playground.webp new file mode 100644 index 00000000..a0fb99f6 Binary files /dev/null and b/docs/docs/meshcentral/images/gc-playground.webp differ diff --git a/docs/docs/meshcentral/index.md b/docs/docs/meshcentral/index.md index be0834e7..b25ac0f7 100644 --- a/docs/docs/meshcentral/index.md +++ b/docs/docs/meshcentral/index.md @@ -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) @@ -213,7 +213,7 @@ As indicated before, the settings section of the config.json is equivalent to pa | RedirPort | This is the port for redirecting traffic in the web server. When the server is configured with HTTPS, users that uses HTTP will be redirected to HTTPS. Port 80 is the default port. So, redirection will happen from port 80 to port 443. | | MpsPort | Port for Intel" AMT Management Presence Server to receive Intel" AMT CIRA (Client Initiated Remote Access) connections. The default is port 4433. This port is disabled in LAN mode. If user don"t plan on using Intel" AMT for management, this port can be left as-is. | | TLSOffload | By default this option is set to "false". If set to "true", server will run both web port and the Intel AMT MPS port without TLS with the assumption that a TLS offloading is taking care of this task. For further details, see the "TLS Offloading" section. This option can also be set to the IP address of the reverse-proxy in order to indicate to MeshCental to only trust HTTP X-Forwarded headers coming from this IP address. See the "Reverse-Proxy Setup" section for an example. | -| SelfUpdate | When set to "true" the server will check for a new version and attempt to self-update automatically a bit after midnight local time every day. For this to work, the server needs to run with sufficient permissions to overwrite its own files. If you run the server with more secure, restricted privileges, this option should not be used. If set to a specific version such as "0.2.7-g" when the server will immediately update to the specified version on startup if it"s not already at this version. | +| SelfUpdate | When set to "true" the server will check for a new version and attempt to self-update automatically a bit after midnight local time every day. If set to a specific version such as "1.1.21" the server will immediately update to the specified version on startup if it's not already at this version. | | SessionKey | This is the encryption key used to secure the user"s login session. It will encrypt the browser cookie. By default, this value is randomly generated each time the server starts. If many servers are used with a load balancer, all servers should use the same session key. In addition, one can set this key so that when the server restarts, users do not need to re-login to the server. | | Minify | Default value is 0, when set to 1 the server will serve "minified" web pages, that is, web pages that have all comments, white spaces and other unused characters removed. This reduces the data size of the web pages by about half and reduced the number requests made by the browser. The source code of the web page will not be easily readable, adding "&nominify=1" at the end of the URL will override this option. | | User | Specify a username that browsers will be automatically logged in as. Useful to skip the login page and password prompts. Used heavily during development of MeshCentral. | @@ -305,6 +305,120 @@ When the MongoDB is setup for the first time, a unique identifier is generated a Once peered, all of the servers should act like one single host, no matter which server the user(s) are connected to. +## Email Setup + +We highly recommend the use of an email server (SMTP) because we could allow MeshCentral to verify user account’s email address by sending a confirmation request to the user to complete the account registration and for password recovery, should a user forget account password as illustrated below + +A verification email is sent when a new account is created or if the user requests it in the “My Account” tab. + +![](images/2022-05-19-00-00-05.png) + +The password recovery flow when “Reset Account” is triggered at the login page. + +![](images/2022-05-19-00-00-18.png) + +Both account verification and password recovery are triggered automatically once SMTP mail server configuration is included into the config.json file. + +#### SMTP: User/Pass +##### Normal Server + +Update the config.json with “smtp” section as shown below and restart the server. + +```json +{ + "smtp": { + "host": "smtp.server.com", + "port": 25, + "from": "myaddress@server.com", + "user": "myaddress@server.com", # Optional + "pass": "mypassword", # Optional + "tls": false # Optional, default false + } +} +``` + +Please map the host, port values to connect to the right host that provides this SMTP service. For “from” value, administrators may put something like donotreply@server.com, but often times it needs to be a valid address since SMTP server will not send out messages with an invalid reply address. + +Some SMTP servers will require a valid username and password to login to the mail server. This is to prevent unauthorized e-mail correspondence. TLS option can be set to ‘true’ if the SMTP server requires TLS. + +##### Gmail + +One option is to configure MeshCentral work with Google Gmail by setting “host” with smtp.gmail.com, and “port” with 587. In the config.json file, use user’s Gmail address for both “from” and “user” and Gmail password in the “pass” value. You will also need to enable “Less secure app access” in for this Google account. It’s in the account settings, security section: + +![](images/2022-05-19-00-01-19.png) + +If a Google account is setup with 2-factor authentication, the option to allow less secure applications not be available. Because the Google account password is in the MeshCentral config.json file and that strong authentication can’t be used, it’s preferable to use a dedicated Google account for MeshCentral email. + +#### SMTP: OAuth Authentication +##### Gmail + +Google has announced that less secure app access will be phased out. For Google Workspace or G-Suite accounts, the following process can be used to allow OAuth2 based authentication with Google's SMTP server. It is likely a very similar process for regular Gmail accounts. + +Start by visiting the Google API console: + +https://console.developers.google.com/ + +First, you will create a new project. Name it something unique in case you need to create more in the future. In this example, I've named the project "MeshCentral" + +![](images/gc-newproject.png) + +Click on the "OAuth Consent Screen" link, Under "APIs and Services" from the left hand menu: + +![](images/gc-oauthconsent.png) + +If you have a Google Workspace account, you will have the option to choose "Internal" application and skip the next steps. If not, you will be required to provide Google with information about why you want access, as well as verifying domain ownership. + +![](images/OAuth-Internal-External.png) + +Add the Gmail address under which you have created this project to the fields labelled ‘User support email’ and ‘Developer contact information’ so that you will be allowed for authentication. After that, you will want to add a scope for your app, so that your token is valid for gmail: + +![](images/gc-oauthscopes.png) + +Once this is complete, the next step will be to add credentials. + +![](images/gc-oauthcredentials.png) + +Choose OAuth Client + +You will obtain a Client ID and a Client secret once you've completed the process. Be sure to store the secret immediately, as you won't be able to retreive it after you've dismissed the window. + +Next, you will need to visit the Google OAuth Playground: + +https://developers.google.com/oauthplayground + +![](images/gc-playground.webp) + +Enter your Client ID and secret from the last step. On the left side of the page, you should now see a text box that allows you to add your own scopes. Enter https://mail.google.com and click Authorize API. + +You will need to follow the instructions provided to finish the authorization process. Once that is complete, you should receive a refresh token. The refresh token, Client ID and Client Secret are the final items we need to complete the SMTP section of our config.json. It should now look something like this: + +``` +"smtp": { + "host": "smtp.gmail.com", + "port": 587, + "from": "my@googleaccount.com", + "auth": { + "clientId": "", + "clientSecret": "", + "refreshToken": "" + }, + "user": "noreply@authorizedgooglealias.com", + "emailDelaySeconds": 10, + "tls": false, + "verifyEmail": true + } +``` + + +Regardless of what SMTP account is used, MeshCentral will perform a test connection to make sure the server if working as expected when starting. Hence, the user will be notified if Meshcentral and SMTP server has been configured correctly as shown below. + +![](images/2022-05-19-00-01-43.png) + +After successfully configuring the Gmail SMTP server, switch the OAuth 'Publishing Status' from `Testing` to `In Production`. This step prevents the need for frequent refresh token generation. Verification of your project isn't required to make this change. + +![](images/In-production.png) + + ## Database A critical component of MeshCentral is the database. The database stores all of the user account information, groups and node data, historical power and event, etc. By default MeshCentral uses NeDB (https://github.com/louischatriot/nedb) that is written entirely in NodeJS and is setup automatically when MeshCentral is installed with the npm tool. The file “meshcentral.db” will be created in the “meshcentral-data” folder when MeshCentral is first launched. This database works well for small deployments scenarios. @@ -438,14 +552,14 @@ To make this happen, we will be using the following command line options from Me | --dblistconfigfiles | List the names and size of all configuration files in the database. | | --dbshowconfigfile (filename) | Show the content of a specified filename from the database. --configkey is required. | | --dbdeleteconfigfiles | Delete all configuration files from the database. | -| --dbpushconfigfiles (*) or (folder path) | Push a set of configuration files into the database, removing any existing files in the process. When * is specified, the “meshcentral-data” folder up pushed into the database. --configkey is required. | +| --dbpushconfigfiles '*' or (folder path) | Push a set of configuration files into the database, removing any existing files in the process. When * is specified, the “meshcentral-data” folder up pushed into the database. --configkey is required. | | --dbpullconfigfiles (folder path) | Get all of the configuration files from the database and place them in the specified folder. Files in the target folder may be overwritten. --configkey is required. | | --loadconfigfromdb (key) | Runs MeshCentral server using the configuration files found in the database. The configkey may be specified with this command or --configkey can be used. | Once we have MeshCentral running as expected using the “meshcentral-data” folder, we can simply push that configuration into the database and run using the database alone like this: ``` -node ./node_modules/meshcentral --dbpushconfigfiles * --configkey mypassword +node ./node_modules/meshcentral --dbpushconfigfiles '*' --configkey mypassword node ./node_modules/meshcentral --loadconfigfromdb mypassword --mongodb "mongodb://127.0.0.1:27017/meshcentral" ``` @@ -609,46 +723,6 @@ All the lines that start with a number or `:` will be used, everything else is i 95.85.81.0/24 ``` -## Email Setup - -We highly recommend the use of an email server (SMTP) because we could allow MeshCentral to verify user account’s email address by sending a confirmation request to the user to complete the account registration and for password recovery, should a user forget account password as illustrated below - -A verification email is sent when a new account is created or if the user requests it in the “My Account” tab. - -![](images/2022-05-19-00-00-05.png) - -The password recovery flow when “Reset Account” is triggered at the login page. - -![](images/2022-05-19-00-00-18.png) - -Both account verification and password recovery are triggered automatically once SMTP mail server configuration is included into the config.json file. Update the config.json with “smtp” section as shown below and restart the server. - -```json -{ - "smtp": { - "host": "smtp.server.com", - "port": 25, - "from": "myaddress@server.com", - "user": "myaddress@server.com",  Optional - "pass": "mypassword",  Optional - "tls": false  Optional, default false - } -} -``` - -Please map the host, port values to connect to the right host that provides this SMTP service. For “from” value, administrators may put something like donotreply@server.com, but often times it needs to be a valid address since SMTP server will not send out messages with an invalid reply address. - -Some SMTP servers will require a valid username and password to login to the mail server. This is to prevent unauthorized e-mail correspondence. TLS option can be set to ‘true’ if the SMTP server requires TLS. - -One option is to configure MeshCentral work with Google Gmail* by setting “host” with smtp.gmail.com, and “port” with 587. In the config.json file, use user’s Gmail* address for both “from” and “user” and Gmail* password in the “pass” value. You will also need to enable “Less secure app access” in for this Google account. It’s in the account settings, security section: - -![](images/2022-05-19-00-01-19.png) - -If a Google account is setup with 2-factor authentication, the option to allow less secure applications not be available. Because the Google account password is in the MeshCentral config.json file and that strong authentication can’t be used, it’s preferable to use a dedicated Google account for MeshCentral email. - -Regardless of what SMTP account is used, MeshCentral will perform a test connection to make sure the server if working as expected when starting. Hence, the user will be notified if Meshcentral and SMTP server has been configured correctly as shown below. - -![](images/2022-05-19-00-01-43.png) ## Embedding MeshCentral @@ -1204,6 +1278,8 @@ And taking authentication to the next step is removing the login page entirely.
+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 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. @@ -1659,7 +1735,39 @@ Enabling SAML will require MeshCentral to install extra modules from NPM, so dep !!!note MeshCentral only supports "POST". [For example Authentik's](https://github.com/Ylianst/MeshCentral/issues/4725) default setting is to use "Redirect" as a "Service Provider Binding". - + +### Generic OpenID Connect Setup + +Generally, if you are using an IdP that supports OpenID Connect (OIDC), you can use a very basic configuration to get started, and if needed, add more specific or advanced configurations later. Here is what your config file will look like with a basic, generic, configuration. + +``` json +{ + "settings": { + "cert": "mesh.your.domain", + "port": 443, + "sqlite3": true + }, + "domains": { + "": { + "title": "Mesh", + "title2": ".Your.Domain", + "authStrategies": { + "oidc": { + "issuer": "https://sso.your.domain", + "clientid": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "clientsecret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "newAccounts": true + } + } + } + } +} +``` + +As you can see, this is roughly the same as all the other OAuth2 based authentication strategies. These are the basics you need to get started using OpenID Connect because it's still authenticating with OAuth2. If you plan to take advantage of some of the more advanced features provided by this strategy you should consider reading the [additional strategy documentation](./openidConnectStrategy.md). + +> NOTE: MeshCentral will use `https://mesh.your.domain/auth-oidc-callback` as the default redirect uri. + ## Improvements to MeshCentral In 2007, the first version of MeshCentral was built. We will refer to it as “MeshCentral1”. When MeshCentral1 was designed, HTML5 did not exist and web sockets where not implemented in any of the major browsers. Many design decisions were made at the time that are no longer optimal today. With the advent of the latest MeshCentral, MeshCentral1 is no longer supported and MeshCentral v2 has been significantly redesigned and mostly re-written based of previous version. Here is a list of improvements made in MeshCentral when compared with MeshCentral1: diff --git a/docs/docs/meshcentral/openidConnectStrategy.md b/docs/docs/meshcentral/openidConnectStrategy.md new file mode 100644 index 00000000..4b6e4911 --- /dev/null +++ b/docs/docs/meshcentral/openidConnectStrategy.md @@ -0,0 +1,671 @@ +# Using the OpenID Connect Strategy on MeshCentral + +## Overview + +### Introduction + +There is a lot of information to go over, but first, why OpenID Connect? + +Esentially its because its both based on a industry standard authorization protocol, and is becoming an industry standard authentication protocol. Put simply it's reliable and reusable, and we use OpenID Connect for exactly those reasons, almost every everyone does, and we want to be able to integrate with almost anyone. This strategy allows us to expand the potential of MeshCentral through the potential of OpenID Connect. + +In this document, we will learn about the OpenID Connect specification at a high level, and then use that information to configure the OpenID Connect strategy for MeshCentral using a generic OpenID Connect compatible IdP. After that we will go over some advanced configurations and then continue by explaining how to use the new presets for popular IdPs, specifically Google or Azure. Then we will explore the configuration and usage of the groups feature. + +> ATTENTION: As of MeshCentral `v1.1.22` there are multiple config options being depreciated. Using any of the old configs will only generate a warning in the authlog and will not stop you from using this strategy at this time. If there is information found in both the new and old config locations the new config location will be used. We will go over the specifics later, now lets jump in. + +### Chart of Frequently Used Terms and Acronyms +| Term | AKA | Descriptions | +| --- | --- | --- | +| OAuth 2.0 | OAuth2 | OAuth 2.0 is the industry-standard protocol for user *authorization*. | +| OpenID Connect | OIDC | Identity layer built on top of OAuth2 for user *authentication*. | +| Identity Provider | IdP | The *service used* to provide authentication and authorization. | +| Preset Configs | Presets | Set of *pre-configured values* to allow some specific IdPs to connect correctly. | +| OAuth2 Scope | Scope | A flag *requesting access* to a specific resource or endpoint | +| OIDC Claim | Claim | A *returned property* in the user info provided by your IdP | +| User Authentication | AuthN | Checks if you *are who you say you are*. Example: Username and password authentication | +| User Authorization | AuthZ | Check if you have the *permissions* required to access a specific resource or endpoint | + +### OpenID Connect Technology Overview + +OpenID Connect is a simple identity layer built on top of the OAuth2 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an “Authorization Server”, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner. + +OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and logout, when it makes sense for them. + +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 + +### *Introduction* + +Generally, if you are using an IdP that supports OIDC, you can use a very basic configuration to get started, and if needed, add more specific or advanced configurations later. Here is what your config file will look like with a basic, generic, configuration. + +### *Basic Config File Example* + +``` json +{ + "settings": { + "cert": "mesh.your.domain", + "port": 443, + "sqlite3": true + }, + "domains": { + "": { + "title": "MeshCentral", + "title2": "Your sub-title", + "authStrategies": { + "oidc": { + "issuer": "https://sso.your.domain", + "clientid": "2d5685c5-0f32-4c1f-9f09-c60e0dbc948a", + "clientsecret": "7PiGSLSLL4e7NGi67KM229tfK7Z7TqzQ", + "newAccounts": true + } + } + } + } +} +``` + +As you can see, this is roughly the same as all the other OAuth2 based authentication strategies. These are the basics you need to get started, however, if you plan to take advantage of some of the more advanced features provided by this strategy, you'll need to keep reading. + +In this most basic of setups, you only need the URL of the issuer, as well as a client ID and a client secret. Notice in this example that the callback URL (or client redirect uri) is not configured, thats because MeshCentral will use `https://mesh.your.domain/auth-oidc-callback` as the default. Once you've got your configuration saved, restart MeshCentral and you should see an OpenID Connect Single Sign-on button on the login screen. + +> WARNING: The redirect endpoint must EXACTLY match the value provided to your IdP or your will deny the connection. + +> ATTENTION: You are required to configure the cert property in the settings section for the default domain, and configure the dns property under each additional domain. + +## Advanced Options + +### Overview + +There are plenty of options at your disposal if you need them. In fact, you can configure any property that node-openid-client supports. The openid-client module supports far more customization than I know what to do with, if you want to know more check out [node-openid-client on GitHub](https://github.com/panva/node-openid-client) for expert level configuration details. There are plenty of things you can configure with this strategy and there is a lot of decumentation behind the tools used to make this all happen. I strongly recommend you explore the [config schema](https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json), and if you have a complicated config maybe check out the [openid-client readme](https://github.com/panva/node-openid-client/blob/main/docs/README.md). Theres a list of resources at the end if you want more information on any specific topics. In the meantime, let’s take a look at an example of what your config file could look with a slightly more complicated configuration, including multiple manually defined endpoints. + +#### *Advanced Config File Example* + +``` json +{ + "settings": { + "cert": "mesh.your.domain", + "port": 443, + "redirPort": 80, + "AgentPong": 300, + "TLSOffload": "192.168.1.50", + "SelfUpdate": false, + "AllowFraming": false, + "sqlite3": true, + "WebRTC": true + }, + "domains": { + "": { + "title": "Mesh", + "title2": ".Your.Domain", + "orphanAgentUser": "~oidc:e48f8ef3-a9cb-4c84-b6d1-fb7d294e963c", + "authStrategies": { + "oidc": { + "issuer": { + "issuer": "https://sso.your.domain", + "authorization_endpoint": "https://auth.your.domain/auth-endpoint", + "token_endpoint": "https://tokens.sso.your.domain/token-endpoint", + "end_session_endpoint": "https://sso.your.domain/logout", + "jwks_uri": "https://sso.your.domain/jwks-uri" + }, + "client": { + "client_id": "110d5612-0822-4449-a057-8a0dbe26eca5", + "client_secret": "4TqST46K53o3Z2Q88p39YwR6YwJb7Cka", + "redirect_uri": "https://mesh.your.domain/auth-oidc-callback", + "post_logout_redirect_uri": "https://mesh.your.domain/login", + "token_endpoint_auth_method": "client_secret_post", + "response_types": "authorization_code" + }, + "custom": { + "scope": [ "openid", "profile", "read.EmailAlias", "read.UserProfile" ], + "preset": null + }, + "groups": { + "recursive": true, + "required": ["Group1", "Group2"], + "siteadmin": ["GroupA", "GroupB"], + "revokeAdmin": true, + "sync": { + "filter": ["Group1", "GroupB", "OtherGroup"] + }, + "claim": "GroupClaim", + "scope": "read.GroupMemberships" + }, + "logouturl": "https://sso.your.domain/logout?r=https://mesh.your.domain/login", + "newAccounts": true + }, + {...} + } + } + } +} +``` + +### "Issuer" Options + +#### *Introduction* + +In the advanced example config above, did you notice that the issuer property has changed from a *string* to an *object* compared to the basic example? This not only allows for much a much smaller config footprint when advanced issuer options are not required, it successfully fools you in to a false sense of confidence early on in this document. If you are manually configuring the issuer endpoints, keep in mind that MeshCentral will still attempt to discover **ALL** issuer information. Obviously if you manually configure an endpoint, it will be used even if the discovered information is different from your config. + +> NOTE: If you are using a preset, you dont need to define an issuer. If you do, the predefined information will be ignored. + +#### *Common Config Chart* + +| Name | Description | Default | Example | Required | +| --- | --- | --- | --- | --- | +| `issuer` | The primary URI that represents your Identity Providers authentication endpoints. | N/A | `"issuer": "https://sso.your.domain"`
`"issuer": { "issuer": "https://sso.your.domain" }` | Unless using preset. | + +#### *Advanced Config Example* + +``` json +"issuer": { + "issuer": "https://sso.your.domain", + "authorization_endpoint": "https://auth.your.domain/auth-endpoint", + "token_endpoint": "https://tokens.sso.your.domain/token-endpoint", + "end_session_endpoint": "https://sso.your.domain/logout", + "jwks_uri": "https://sso.your.domain/jwks-uri" +}, +``` + +#### *Required and Commonly Used Configs* + +The `issuer` property in the `issuer` object is the only one required, and its only required if you aren't using a preset. Besides the issuer, these are mostly options related to the endpoints and their configuration. The schema below looks intimidating but it comes down to being able to support any IdP. Setting the issuer, and end_session_endpoint are the two main ones you want to setup. + +#### *Schema* + +``` json +"issuer": { + "type": ["string","object"], + "format": "uri", + "description": "Issuer options. Requires issuer URI (issuer.issuer) to discover missing information unless using preset", + "properties": { + "issuer": { "type": "string", "format": "uri", "description": "URI of the issuer." }, + "authorization_endpoint": { "type": "string", "format": "uri" }, + "token_endpoint": { "type": "string", "format": "uri" }, + "jwks_uri": { "type": "string", "format": "uri" }, + "userinfo_endpoint": { "type": "string", "format": "uri" }, + "revocation_endpoint": { "type": "string", "format": "uri" }, + "introspection_endpoint": { "type": "string", "format": "uri" }, + "end_session_endpoint": { + "type": "string", + "format": "uri", + "description": "URI to direct users to when logging out of MeshCentral.", + "default": "this.issuer/logout" + }, + "registration_endpoint": { "type": "string", "format": "uri" }, + "token_endpoint_auth_methods_supported": { "type": "string" }, + "token_endpoint_auth_signing_alg_values_supported": { "type": "string" }, + "introspection_endpoint_auth_methods_supported": { "type": "string" }, + "introspection_endpoint_auth_signing_alg_values_supported": { "type": "string" }, + "revocation_endpoint_auth_methods_supported": { "type": "string" }, + "revocation_endpoint_auth_signing_alg_values_supported": { "type": "string" }, + "request_object_signing_alg_values_supported": { "type": "string" }, + "mtls_endpoint_aliases": { + "type":"object", + "properties": { + "token_endpoint": { "type": "string", "format": "uri" }, + "userinfo_endpoint": { "type": "string", "format": "uri" }, + "revocation_endpoint": { "type": "string", "format": "uri" }, + "introspection_endpoint": { "type": "string", "format": "uri" } + } + } + }, + "additionalProperties": false +}, +``` + +### "Client" Options + +#### *Introduction* + +There are just about as many option as possible here since openid-client also provides a Client class, because of this you are able to manually configure the client how ever you need. This includes setting your redirect URI to any available path, for example, if I was using the "google" preset and wanted to have Google redirect me back to "https://mesh.your.domain/oauth2/oidc/redirect/givemebackgooglemusicyoujerks", MeshCentral will now fully support you in that. One of the other options is the post logout redirect URI, and it is exactly what it sounds like. After MeshCentral logs out a user using the IdPs end session endpoint, it send the post logout redirect URI to your IdP to forward the user back to MeshCentral or to an valid URI such as a homepage. + +> NOTE: The client object is required, however an exception would be with using old configs, which will be discussed later. + +#### *Common Configs* + +| Name | Description | Default | Example | Required | +| --- | --- | --- | --- | --- | +| `client_id` | The client ID provided by your Identity Provider (IdP) | N/A | `bdd6aa4b-d2a2-4ceb-96d3-b3e23cd17678` | `true` | +| `client_secret` | The client secret provided by your Identity Provider (IdP) | N/A | `vUg82LJ322rp2bvdzuVRh3dPn3oVo29m` | `true` | +| `redirect_uri` | "URI your IdP sends you after successful authorization. | `https://mesh.your.domain/auth-oidc-callback` | `https://mesh.your.domain/oauth2/oidc/redirect` | `false` | +| `post_logout_redirect_uri` | URI for your IdP to send you after logging out of IdP via MeshCentral. | `https://mesh.your.domain/login` | `https://site.your.other.domain/login` | `false` | + +#### *Advanced Config Example* + +``` json +"client": { + "client_id": "00b3875c-8d82-4238-a8ef-25303fa7f9f2", + "client_secret": "7PP453H577xbFDCqG8nYEJg8M3u8GT8F", + "redirect_uri": "https://mesh.your.domain/auth-oidc-callback", + "post_logout_redirect_uri": "https://mesh.your.domain/login", + "token_endpoint_auth_method": "client_secret_post", + "response_types": "authorization_code" +}, +``` + +#### *Required and Commonly Used Configs* + +There are many available options you can configure but most of them go unused. Although there are a few *commonly used* properties. The first two properties, `client_id` and `client_secret` are required. The next one `redirect_uri` is used to setup a custom URI for the redirect back to MeshCentral after being authenicated by your IdP. The `post_logout_redirect_uri` property is used to tell your IdP where to send you after being logged out. These work in conjunction with the issuers `end_session_url` to automatically fill in any blanks in the config. + +#### *Schema* +``` json +"client": { + "type": "object", + "description": "OIDC Client Options", + "properties": { + "client_id": { + "type": "string", + "description": "REQUIRED: The client ID provided by your Identity Provider (IdP)" + }, + "client_secret": { + "type": "string", + "description": "REQUIRED: The client secret provided by your Identity Provider (IdP)" + }, + "redirect_uri": { + "type": "string", + "format": "uri", + "description": "URI your IdP sends you after successful authorization. This must match what is listed with your IdP. (Default is https://[currentHost][currentPath]/auth-oidc-callback)" + }, + "post_logout_redirect_uri": { + "type": "string", + "format": "uri", + "description": "URI for your IdP to send you after logging out of IdP via MeshCentral.", + "default": "https:[currentHost][currentPath]/login" + }, + "id_token_signed_response_alg": { "type": "string", "default": "RS256" }, + "id_token_encrypted_response_alg": { "type": "string" }, + "id_token_encrypted_response_enc": { "type": "string" }, + "userinfo_signed_response_alg": { "type": "string" }, + "userinfo_encrypted_response_alg": { "type": "string" }, + "userinfo_encrypted_response_enc": { "type": "string" }, + "response_types": { "type": ["string", "array"], "default": ["code"] }, + "default_max_age": { "type": "number" }, + "require_auth_time": { "type": "boolean", "default": false }, + "request_object_signing_alg": { "type": "string" }, + "request_object_encryption_alg": { "type": "string" }, + "request_object_encryption_enc": { "type": "string" }, + "token_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ] + }, + "introspection_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ] + }, + "revocation_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ] + }, + "token_endpoint_auth_signing_alg": { "type": "string" }, + "introspection_endpoint_auth_signing_alg": { "type": "string" }, + "revocation_endpoint_auth_signing_alg": { "type": "string" }, + "tls_client_certificate_bound_access_tokens": { "type": "boolean" } + }, + "required": [ "client_id", "client_secret" ], + "additionalProperties": false +}, +``` + +### "Custom" Options + +#### *Introduction* + +These are all the options that dont fit with the issuer or client, including the presets. The presets define more than just the issuer URL used in discovery, they also define API endpoints, and specific ways to assemble your data. You are able to manually override most of the effects of the preset, but not all. You are able to manually configure the *scope* of the authorization request though, as well as choose which claims to use if your IdP uses something other than the defaults. + +> NOTE: The scope must be a string, an array of strings, or a space separated list of scopes as a single string. + +#### *Common Config Chart* + +| Name | Description | Default | Example | Required | +| -------- | ------------------------------------------------ | --------------------------------------------------------- | ----------------------------------- | -------- | +| `scope` | A list of scopes to request from the issuer. | `"openid profile email"` | `["openid", "profile"]` | `false` | +| `claims` | A group of claims to use instead of the defaults | Defauts to name of property except that `uuid` used `sub` | `"claims": {"uuid": "unique_name"}` | `false` | + +#### *Advanced Config Example* + +``` json +"custom": { + "scope": [ "openid", "profile", "read.EmailAlias", "read.UserProfile" ], + "preset": null, + "claims": { + "name": "nameOfUser", + "email": "publicEmail" + } +}, +``` + +> NOTE: You can `preset` to null if you want to explicitly disable presets. + +#### *Required and Commonly Used Configs* + +As should be apparent by the name alone, the custom property does not need to be configured and is used for optional or advanced configurations. With that said, lets look at few common options strategy will default to using the `openid`, `profile`, and `email` scopes to gather the required information about the user, if your IdP doesn't support or require all these, you can set up the scope manually. Combine that with the ability to set the group scope and you can end up with an entirely custom scope being sent to your IdP. Not to mention the claims property, which allows you to pick and choose what claims to use to gather your data in case you have issues with any of the default behaviors of OpenID Connect and your IdP. This is also where you would set the preset and any values required by the presets. + +#### *Schema* +``` json +"custom": { + "type": "object", + "properties": { + "scope": { + "type": ["string", "array"], + "description": "A list of scopes to request from the issuer.", + "default": "openid profile email", + "examples": ["openid", ["openid", "profile"], "openid profile email", "openid profile email groups"] + }, + "claims": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "name": { "type": "string" }, + "uuid": { "type": "string" } + } + }, + "preset": { "type": "string", "enum": ["azure", "google"]}, + "tenant_id": { "type": "string", "description": "REQUIRED FOR AZURE PRESET: Tenantid for Azure"}, + "customer_id": { "type": "string", "description": "REQUIRED FOR GOOGLE PRESET IF USING GROUPS: Customer ID from Google, should start with 'C'."} + }, + "additionalProperties": false +}, +``` + +### "Groups" Options + +#### *Introduction* + +The groups option allows you to use the groups you already have with your IdP in MeshCentral in a few ways. First you can set a group that the authorized user must be in to sign in to MeshCentral. You can also allow users with the right memberships automatic admin privlidges, and there is even an option to revoke privlidges if the user is NOT in the admin group. Besides these filters, you can filter the sync property to mirror only certain groups as MeshCentral User Groups, dynamically created as the user logs in. You can of course simply enable sync and mirror all groups from your IdP as User Groups. Additionally you can define the scope and claim of the groups for a custom setup, again allowing for a wide range of IdPs to be used, even without a preset. + +#### *Common Config Chart* + +| Name | Description | Default | Example | Required | +| --- | --- | --- | --- | --- | +| `sync` | Allows you to mirror user groups from your IdP. | `false` | `"sync": { "filter": ["Group1", "Group2"] }`
`"sync": true` | `false` | +| `required` | Access is only granted to users who are a member
of at least one of the listed required groups. | `undefined` | `"required": ["Group1", "Group2"]` | `false` | +| `siteadmin` | Full site admin priviledges will be granted to users
who are a member of at least one of the listed admin groups | `undefined` | `"siteadmin": ["Group1", "Group2"]` | `false` | +| `revokeAdmin` | If true, admin privileges will be revoked from users
who arent a member of at least one of the listed admin groups. | `true` | `"revokeAdmin": false` | `false` | + +#### *Advanced Config Example* + +``` json +"groups": { + "recursive": true, + "required": ["Group1", "Group2"], + "siteadmin": ["GroupA", "GroupB"], + "revokeAdmin": false, + "sync": { + "filter": ["Group1", "GroupB", "OtherGroup"] + }, + "claim": "GroupClaim", + "scope": "read.GroupMemberships" +}, +``` + +#### *Required and Commonly Used Configs* + +As you can see in the schema below, there aren't any required properties in the groups object, however there are some commonly used ones. The first, and maybe most commonly used one, is the sync property. The sync property mirrors IdP provided groups into MeshCentral as user groups. You can then configure access as required to those groups, and as users log in, they will be added to the now existing groups if they are a member. You also have other options like using a custom *scope* or *claim* to get your IdP communicating with MeshCentral properly, without the use of preset configs. You also can set the required property if you need to limit authorization to users that are a member of at least one of the groups you set. or the siteadmin property to grant admin privilege, with the revokeAdmin property available to allow revoking admin rights also. + +#### *Schema* + +``` json +"groups": { + "type": "object", + "properties": { + "recursive": { + "type": "boolean", + "default": false, + "description": "When true, the group memberships will be scanned recursively." + }, + "required": { + "type": [ "string", "array" ], + "description": "Access is only granted to users who are a member of at least one of the listed required groups." + }, + "siteadmin": { + "type": [ "string", "array" ], + "description": "Full site admin priviledges will be granted to users who are a member of at least one of the listed admin groups." + }, + "revokeAdmin": { + "type": "boolean", + "default": false, + "description": "If true, admin privileges will be revoked from users who are NOT a member of at least one of the listed admin groups." + }, + "sync": { + "type": [ "boolean", "object" ], + "default": false, + "description": "If true, all groups found during user login are mirrored into MeshCentral user groups.", + "properties": { + "filter": { + "type": [ "string", "array" ], + "description": "Only groups listed here are mirrored into MeshCentral user groups." + } + } + }, + "scope": { "type": "string", "default": "groups", "description": "Custom scope to use." }, + "claim": { "type": "string", "default": "groups", "description": "Custom claim to use." } + }, + "additionalProperties": false +} +``` + +## Preset OpenID Connect Configurations + +### Overview + +#### *Introduction* + +Google is a blah and is used by tons of blahs as its so great. Lets move on. + +#### *Common Config Chart* + +> NOTE: All settings directly related to presets are in the custom section of the config. + +| Name | Description | Example | Required | +| --- | --- | --- | --- | +| `preset` | Manually enable the use of a preset. | `"preset": "google"`
`"preset": "azure"` | `false` | +| `customer_id` | Customer ID of the Google Workspaces instace you
plan to use with the groups feature.| `"customer_id": ["Group1", "Group2"]` | If `google` preset is used with `groups` feature | +| `tenant_id` | Tenant ID from Azure AD, this is required to use
the `azure` preset as it is part of the issuer url. | `"siteadmin": ["Group1", "Group2"]` | `false` | + +### Google Preset + +#### *Prerequisites* + +> Check out this [documentation](https://developers.google.com/identity/protocols/oauth2/openid-connect) to get ready before we start. + +#### *Basic Config Example* + +``` json +"oidc": { + "client": { + "client_id": "268438852161-r8xa7qxwf3rr0shp1xnpgmm70bnag21p.apps.googleusercontent.com", + "client_secret": "ETFWBX-gFEaxfPXs1tWmAOkuWDFTgoL3nwh" + } +} +``` + +#### *Specifics* + +If you notice above I forgot to add any preset related configs, however because google tags the client ID we can detect that and automatically use the google preset. The above config is tested, the sentive data has been scrambled of course. That said, you would normally use this preset in more advaced setups, let take a look at an example. + +#### *Advanced Example with Groups* + +``` json +"oidc": { + "client": { + "client_id": "424555768625-k7ub3ovqs0yp7mfo0usvyyx51nfii61c.apps.googleusercontent.com", + "client_secret": "QLBCQY-nRYmjnFWv3nKyHGmwQEGLokP6ldk" + }, + "custom": { + "preset": "google", + "customer_id": "C46kyhmps" + }, + "groups": { + "siteadmin": ["GroupA", "GroupB"], + "revokeAdmin": true, + "sync": true + }, + "callbackURL": "https://mesh.your.domain/auth-oidc-google-callback" +}, +``` + +#### *Customer ID and Groups* + +As always, the client ID and secret are required, the customer ID on the other hand is only required if you plan to take advantage of the groups function *and* the google preset. This also requires you have a customer ID, if you have do, it is available in the Google Workspace Admin Console under Profile->View. Groups work the same as they would with any other IdP but they are pulled from the Workspace groups. + +#### *Schema* + +```json +"custom": { + "type": "object", + "properties": { + "preset": { "type": "string", "enum": ["azure", "google"]}, + "customer_id": { "type": "string", "description": "Customer ID from Google, should start with 'C'."} + }, + "additionalProperties": false +}, +``` + +### Azure Preset + +#### *Prerequisites* + +To configure OIDC-based SSO, you need an Azure account with an active subscription. [Create an account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) for free. The account used for setup must be of the following roles: Global Administrator, Cloud Application Administrator, Application Administrator, or owner the service principal. + +> Check this [documentation](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-oidc-sso) for more information. + +#### *Basic Config Example* + +``` json +"oidc": { + "client": { + "client_id": "a1gkl04i-40g8-2h74-6v41-2jm2o2x0x27r", + "client_secret": "AxT6U5K4QtcyS6gF48gndL7Ys22BL15BWJImuq1O" + }, + "custom": { + "preset": "azure", + "tenant_id": "46a6022g-4h33-1451-h1rc-08102ga3b5e4" + } +} +``` + +#### *Specifics* + +As with all other types of configuration for the OIDC strategy, the Azure preset requires a client ID and secret.The tenant ID is used as part of the issuer URI to make even the most basic AuthN requests so it is also required for the azure preset. besides that groups are available to the Azure preset as well as the recursive feature of groups. This allows you to search user groups recursively for groups they have membership in through other groups. + +> NOTE: The Azure AD preset uses the Tenant ID as part of the issuer URI:
`"https://login.microsoftonline.com/"` + `strategy`.custom.tenant_id + `"/v2.0"` + +#### *Advanced Example with Groups* + +``` json +"oidc": { + "client": { + "client_id": "a1gkl04i-40g8-2h74-6v41-2jm2o2x0x27r", + "client_secret": "AxT6U5K4QtcyS6gF48gndL7Ys22BL15BWJImuq1O" + }, + "custom": { + "preset": "azure", + "tenant_id": "46a6022g-4h33-1451-h1rc-08102ga3b5e4" + }, + "groups": { + "recursive": true, + "siteadmin": ["GroupA", "GroupB"], + "revokeAdmin": true, + "sync": true + }, + "callbackURL": "https://mesh.your.domain/auth-oidc-azure-callback" +}, +``` + +#### *Schema* + +```json +"custom": { + "type": "object", + "properties": { + "preset": { "type": "string", "enum": ["azure", "google"]}, + "tenant_id": { "type": "string", "description": "Tenant ID from Azure AD."} + }, + "additionalProperties": false +}, +``` + +## Depreciated Properties + +### Overview + +#### Introduction + +As of MeshCentral `v1.1.22` and the writing of this documentation, the node module that handles everything was changed from [passport-openid-connect](https://github.com/jaredhanson/passport-openidconnect) to [openid-client](https://github.com/panva/node-openid-client). As a result of this change, multiple properties in the config have been depcrecated; this means some options in the strategy arent being used anymore. These are often referred to as "old configs" by this documentation. + +#### *Migrating Old Configs* + +We upgraded but what about all the existing users, we couldn't just invalidate every config pre `v1.1.22`. So in an effort to allow greater flexibility to all users of MeshCentral, and what futures scholars will all agree was an obvious move, all the depreciated configs will continue working as expected. Using any of the old options will just generate a warning in the authlog and will not stop you from using this the OIDC strategy with outdated configs, however if both the equivalent new and old config are set the new config will be used. + +#### *Old Config Example* +```json +"oidc": { + "newAccounts": true, + "clientid": "421326444155-i1tt4bsmk3jm7dri6jldekl86rfpg07r.apps.googleusercontent.com", + "clientsecret": "GNLXOL-kEDjufOCk6pIcTHtaHFOCgbT4hoi" +} +``` + +This example was chosen because I wanted to highlight an advantage of supporting these old configs long term, even in a depreciated status. That is, the ability to copy your existing config from one of the related strategies without making any changes to your config by using the presets. This allows you to test out the oidc strategy without commiting to anything, since the user is always appended with the strategy used to login. In this example, the config was originally a google auth strategy config, changing the `"google"` to `"oidc"` is all that was done to the above config, besides obsfuscation of course. + +#### *Advcanced Old Config Example* + +``` json +"oidc": { + "authorizationURL": "https://sso.your.domain/api/oidc/authorization", + "callbackURL": "https://mesh.your.domain/oauth2/oidc/callback", + "clientid": "tZiPTMDNuSaQPapAQJtwDXVnYjjhQybc", + "clientsecret": "vrQWspJxdVAxEFJdrxvxeQwWkooVcqdU", + "issuer": "https://sso.your.domain", + "tokenURL": "https://sso.your.domain/api/oidc/token", + "userInfoURL": "https://sso.your.domain/api/oidc/userinfo", + "logoutURL": "https://sso.your.domain/logout?rd=https://mesh.your.domain/login", + "groups": { + "recursive": true, + "required": ["Group1", "Group2"], + "siteadmin": ["GroupA", "GroupB"], + "sync": { + "filter": ["Group1", "GroupB", "OtherGroup"] + } + }, + "newAccounts": true +}, +``` + +#### *Upgrading to v1.1.22* + +If you were already using a meticulusly configured oidc strategy, all of your configs will still be used. You will simply see a warning in the logs if any depreciated properties were used. If you check the authLog there are additional details about the old config and provide the new place to put that information. In this advanced config, even the groups will continue to work just as they did before without any user intervention when upgrading from a version of MeshCentral pre v1.1.22. There are no step to take and no action is needed, moving the configs to the new locations is completely optional at the moment. + +# Links + +https://cloud.google.com/identity/docs/reference/rest/v1/groups/list + +https://www.onelogin.com/learn/authentication-vs-authorization + +https://auth0.com/docs/authenticate/protocols/openid-connect-protocol + +https://github.com/panva/node-openid-client + +https://openid.net/connect/ + +> You just read `openidConnectStrategy.ms v1.0.1` by [@mstrhakr](https://github.com/mstrhakr) diff --git a/docs/docs/meshcentral/plugins.md b/docs/docs/meshcentral/plugins.md index e3e34520..4b85b0c5 100644 --- a/docs/docs/meshcentral/plugins.md +++ b/docs/docs/meshcentral/plugins.md @@ -5,7 +5,7 @@ ## Use Cases -Certain feature requests may not be suitable for all MeshCentral users and thus are available as a plugin. Furthermore users can develop their own plugins - as described further below - to extend functionality or benefit from integrating the powerful MeshCentral into their existing application environment much better. +Certain feature requests may not be suitable for all MeshCentral users and thus are available as a plugin. Furthermore users can develop their own plugins - as described further below - to extend functionality or benefit from integrating MeshCentral into their existing application environment. ## List of publically available plugins @@ -17,10 +17,10 @@ Certain feature requests may not be suitable for all MeshCentral users and thus >"plugins": { > "enabled": true >}, -2. Restart MeshCentral in case you just enabled plugins in the configuration. +2. Restart MeshCentral if you needed to change the configuration. 2. Log into MeshCentral as full administrator. -3. Go my `My Server` -> `Plugins`, hit the Download plugin button. -4. A dialog opens requesting an URL, e.g. put in: +3. Go my `My Server` -> `Plugins`, then hit the Download plugin button. +4. A dialog opens requesting a URL, e.g. put in: 5. The plugin pops up in the plugin list below the download button, you can now configure and enable/disable it. # Plugins - Development & Hooks @@ -30,7 +30,7 @@ Certain feature requests may not be suitable for all MeshCentral users and thus ## Overview -Not all feature requests may be suitable for all MeshCentral users and thus can't be integrated into MeshCentral directly. Hwoever, Instead of maintaining a complete fork of MeshCentral it is so much easier to benefit from and extend MeshCentral's functionality by using hooks and writing plugins for it. +Not all feature requests may be suitable for all MeshCentral users and thus can't be integrated into MeshCentral directly. Hwoever, Instead of maintaining a complete fork of MeshCentral it is much easier to extend MeshCentral's functionality using hooks and writing plugins for it. ## Anatomy of a plugin: @@ -78,7 +78,7 @@ A valid JSON object within a file named `config.json` in the root folder of your | configUrl | Yes | string | the URL to the config.json of the project | | downloadUrl | Yes | string | the URL to a ZIP of the project (used for installation/upgrades) | | repository | Yes | JSON object | contains the following attributes | -| repository.type | Yes | string | valid values are `git` and in the future, `npm` will also be supported in the future | +| repository.type | Yes | string | valid values are `git` and in the future, `npm` will also be supported. | | repository.url | Yes | string | the URL to the project's repository | | versionHistoryUrl | No | string | the URL to the project's versions/tags | | meshCentralCompat | Yes | string | the minimum version string of required compatibility with the MeshCentral server, can be formatted as "0.1.2-c" or ">=0.1.2-c". Currently only supports minimum version, not full semantic checking. | @@ -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. +## 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 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. diff --git a/docs/docs/meshcentral/security.md b/docs/docs/meshcentral/security.md index f442a6c5..ef1dc2d7 100644 --- a/docs/docs/meshcentral/security.md +++ b/docs/docs/meshcentral/security.md @@ -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. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b36bb292..8773ed8a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -22,6 +22,7 @@ nav: - 'Tips n Tricks': 'meshcentral/tipsntricks.md' - 'Messaging': 'messaging/index.md' - 'Customization': 'meshcentral/customization.md' + - 'openidConnectStrategy': 'meshcentral/openidConnectStrategy.md' - Design and Architecture: - design/index.md @@ -38,6 +39,9 @@ nav: - Intel AMT: - intelamt/index.md + - How to Contribute: + - how-to-contribute/index.md + - Other: - other/adfs_sso_guide.md - other/meshcentral_satellite.md diff --git a/emails/translations/device-help_es.html b/emails/translations/device-help_es.html index cb1abaf6..beafbccf 100644 --- a/emails/translations/device-help_es.html +++ b/emails/translations/device-help_es.html @@ -1,20 +1,20 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Solicitud de ayuda
+
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Solicitud de Ayuda
- [[[SERVERNAME]]] - Solicitud de ayuda + [[[SERVERNAME]]] - Solicitud de Ayuda

- Device "[[[DEVICENAME]]]" requested help. + Dispositivo "[[[DEVICENAME]]]" ha pedido ayuda.

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] + Usuario: [[[HELPUSERNAME]]]
+ Solicitud: [[[HELPREQUEST]]]

- haz clic aquí to navigate to this device. + haz clic aquí para navegar a este dispositivo.

\ No newline at end of file diff --git a/emails/translations/device-help_es.txt b/emails/translations/device-help_es.txt index 05567864..3dc02cc3 100644 --- a/emails/translations/device-help_es.txt +++ b/emails/translations/device-help_es.txt @@ -1,7 +1,7 @@ -[[[SERVERNAME]]] - Solicitud de ayuda del dispositivo -Device "[[[DEVICENAME]]]" requested assistance. +[[[SERVERNAME]]] - Solicitud de ayuda con el Dispositivo +Dispositivo "[[[DEVICENAME]]]" solicito asistencia. -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" +Usuario: "[[[HELPUSERNAME]]]" +Solicitud: "[[[HELPREQUEST]]]" [[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/firebase.js b/firebase.js index d20a9541..124acfa0 100644 --- a/firebase.js +++ b/firebase.js @@ -1,7 +1,6 @@ /** * @description MeshCentral Firebase communication module * @author Ylian Saint-Hilaire -* @copyright Intel Corporation 2018-2022 * @license Apache-2.0 * @version v0.0.1 */ @@ -14,27 +13,31 @@ /*jshint esversion: 6 */ "use strict"; -// Construct the Firebase object -module.exports.CreateFirebase = function (parent, senderid, serverkey) { - var obj = {}; +// Initialize the Firebase Admin SDK +module.exports.CreateFirebase = function (parent, serviceAccount) { + + // Import the Firebase Admin SDK + const admin = require('firebase-admin'); + + const obj = {}; obj.messageId = 0; obj.relays = {}; obj.stats = { - mode: "Real", + mode: 'Real', sent: 0, sendError: 0, received: 0, receivedNoRoute: 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 if (parent.config.firebase && (parent.config.firebase.log === true)) { obj.logpath = parent.path.join(parent.datapath, 'firebase.txt'); @@ -42,155 +45,108 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { } else { obj.log = function () { } } - - // Messages received from client (excluding receipts) - 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' - + + // Function to send notifications obj.sendToDevice = function (node, payload, options, func) { - 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'); } }) + if (typeof node === 'string') { + 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 { obj.sendToDeviceEx(node, payload, options, func); } - } - + }; + // Send an outbound push notification obj.sendToDeviceEx = function (node, payload, options, func) { - parent.debug('email', 'Firebase-sendToDevice'); - if ((node == null) || (typeof node.pmt != 'string')) return; + if (!node || typeof node.pmt !== 'string') { + func(0, 'error'); + return; + } + obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options)); - + // Fill in our lookup table - if (node._id != null) { tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } } - - // Built the on-screen notification - var notification = null; - if (payload.notification) { - var notification = new Notification('ic_message') - .title(payload.notification.title) - .body(payload.notification.body) - .build(); + if (node._id) { + tokenToNodeMap[node.pmt] = { + nid: node._id, + mid: node.meshid, + did: node.domain + }; } - - // Build the message - var message = new Message('msg_' + (++obj.messageId)); - if (options.priority) { message.priority(options.priority); } - if (payload.data) { for (var i in payload.data) { message.addData(i, payload.data[i]); } } - 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. - if (notification) { message.notification(notification) } - message.build(); - - // Send the message - function callback(result) { - if (result.getError() == null) { obj.stats.sent++; obj.log('Success'); } else { obj.stats.sendError++; obj.log('Fail'); } - callback.func(result.getMessageId(), result.getError(), result.getErrorDescription()) - } - callback.func = func; - parent.debug('email', 'Firebase-sending'); - xcs.sendNoRetry(message, node.pmt, callback); - } - + + const message = { + token: node.pmt, + notification: payload.notification, + data: payload.data, + android: { + priority: options.priority || 'high', + ttl: options.timeToLive ? options.timeToLive * 1000 : undefined + } + }; + + admin.messaging().send(message).then(function (response) { + obj.stats.sent++; + obj.log('Success'); + func(response); + }).catch(function (error) { + obj.stats.sendError++; + obj.log('Fail: ' + error); + func(0, error); + }); + }; + // Setup a two way relay obj.setupRelay = function (ws) { - // Select and set a relay identifier 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; - // On message, parse it ws.on('message', function (msg) { parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg); - if (typeof msg == 'string') { + if (typeof msg === 'string') { obj.log('Relay: ' + msg); - - // Parse the incoming push request - var data = null; - try { data = JSON.parse(msg) } catch (ex) { return; } - if (typeof data != 'object') return; - if (parent.common.validateObjectForMongo(data, 4096) == false) return; // Perform sanity checking on this object. - 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 - obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) { - if (err == null) { - try { wsrelay.send(JSON.stringify({ sent: true })); } catch (ex) { } + + let data; + try { data = JSON.parse(msg); } catch (ex) { return; } + if (typeof data !== 'object') return; + if (!parent.common.validateObjectForMongo(data, 4096)) return; + if (typeof data.pmt !== 'string' || typeof data.payload !== 'object') return; + + data.payload.data = data.payload.data || {}; + data.payload.data.r = ws.relayId; + + obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err) { + if (!err) { + try { ws.send(JSON.stringify({ sent: true })); } catch (ex) { } } else { - try { wsrelay.send(JSON.stringify({ sent: false })); } catch (ex) { } + try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { } } }); } }); - + // If error, close the relay ws.on('error', function (err) { parent.debug('email', 'FBWS-Error(' + this.relayId + '): ' + err); delete obj.relays[this.relayId]; }); - + // Close the relay ws.on('close', function () { parent.debug('email', 'FBWS-Close(' + 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; }; @@ -212,7 +168,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { const querystring = require('querystring'); const relayUrl = require('url').parse(url); parent.debug('email', 'CreateFirebaseRelay-Setup'); - + // Setup logging if (parent.config.firebaserelay && (parent.config.firebaserelay.log === true)) { obj.logpath = parent.path.join(parent.datapath, 'firebaserelay.txt'); @@ -220,7 +176,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { } else { obj.log = function () { } } - + obj.log('Starting relay to: ' + relayUrl.href); if (relayUrl.protocol == 'wss:') { // Setup two-way push notification channel @@ -252,7 +208,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { parent.debug('email', 'FBWS-Disconnected'); obj.wsclient = null; obj.wsopen = false; - + // Compute the backoff timer if (obj.reconnectTimer == null) { if ((obj.lastConnect != null) && ((Date.now() - obj.lastConnect) > 10000)) { obj.backoffTimer = 0; } @@ -263,12 +219,12 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { } }); } - + function processMessage(messageId, from, data, category) { // Lookup node information from the cache var ninfo = obj.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); @@ -276,7 +232,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { obj.stats.receivedBadArgs++; } } - + obj.sendToDevice = function (node, payload, options, func) { 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'); } }) @@ -284,19 +240,19 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { obj.sendToDeviceEx(node, payload, options, func); } } - + obj.sendToDeviceEx = function (node, payload, options, func) { parent.debug('email', 'Firebase-sendToDevice-webSocket'); if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; } obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options)); - + // Fill in our lookup table if (node._id != null) { obj.tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } } - + // Fill in the server agent cert hash if (payload.data == null) { payload.data = {}; } if (payload.data.shash == null) { payload.data.shash = parent.webserver.agentCertificateHashBase64; } // Add the server agent hash, new Android agents will reject notifications that don't have this. - + // If the web socket is open, send now if (obj.wsopen == true) { try { obj.wsclient.send(JSON.stringify({ pmt: node.pmt, payload: payload, options: options })); } catch (ex) { func(0, 'error'); obj.stats.sendError++; return; } @@ -314,7 +270,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { } else if (relayUrl.protocol == 'https:') { // Send an outbound push notification using an HTTPS POST obj.pushOnly = true; - + obj.sendToDevice = function (node, payload, options, func) { 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'); } }) @@ -322,18 +278,18 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { obj.sendToDeviceEx(node, payload, options, func); } } - + obj.sendToDeviceEx = function (node, payload, options, func) { parent.debug('email', 'Firebase-sendToDevice-httpPost'); if ((node == null) || (typeof node.pmt != 'string')) return; - + // Fill in the server agent cert hash if (payload.data == null) { payload.data = {}; } if (payload.data.shash == null) { payload.data.shash = parent.webserver.agentCertificateHashBase64; } // Add the server agent hash, new Android agents will reject notifications that don't have this. - + obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options)); const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) }); - + // Send the message to the relay const httpOptions = { hostname: relayUrl.hostname, @@ -357,6 +313,6 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { req.end(); } } - + return obj; }; \ No newline at end of file diff --git a/letsencrypt.js b/letsencrypt.js index c83cf102..c1823ba4 100644 --- a/letsencrypt.js +++ b/letsencrypt.js @@ -28,6 +28,8 @@ module.exports.CreateLetsEncrypt = function (parent) { obj.challenges = {}; obj.runAsProduction = false; obj.redirWebServerHooked = false; + obj.zerossl = false; + obj.csr = null; obj.configErr = null; obj.configOk = false; obj.pendingRequest = false; @@ -57,6 +59,7 @@ module.exports.CreateLetsEncrypt = function (parent) { // Get the current certificate obj.getCertificate = function(certs, func) { obj.runAsProduction = (obj.parent.config.letsencrypt.production === true); + obj.zerossl = ((typeof obj.parent.config.letsencrypt.zerossl == 'object') ? obj.parent.config.letsencrypt.zerossl : false); obj.log("Getting certs from local store (" + (obj.runAsProduction ? "Production" : "Staging") + ")"); if (certs.CommonName.indexOf('.') == -1) { obj.configErr = "Add \"cert\" value to settings in config.json before using Let's Encrypt."; parent.addServerWarning(obj.configErr); obj.log("WARNING: " + obj.configErr); func(certs); return; } if (obj.parent.config.letsencrypt == null) { obj.configErr = "No Let's Encrypt configuration"; parent.addServerWarning(obj.configErr); obj.log("WARNING: " + obj.configErr); func(certs); return; } @@ -164,26 +167,36 @@ module.exports.CreateLetsEncrypt = function (parent) { obj.log("Generating private key..."); acme.forge.createPrivateKey().then(function (accountKey) { - // TODO: ZeroSSL - // https://acme.zerossl.com/v2/DV90 - // Create the ACME client obj.log("Setting up ACME client..."); - obj.client = new acme.Client({ - directoryUrl: obj.runAsProduction ? acme.directory.letsencrypt.production : acme.directory.letsencrypt.staging, - accountKey: accountKey - }); + if (obj.zerossl) { + if (obj.zerossl.kid == "") { obj.log("EAB KID hasn't been set, invalid configuration."); return; } + if (obj.zerossl.hmackey == "") { obj.log("EAB HMAC KEY hasn't been set, invalid configuration."); return; } + obj.client = new acme.Client({ + directoryUrl: acme.directory.zerossl.production, + accountKey: accountKey, + externalAccountBinding: { + kid: obj.zerossl.kid, + hmacKey: obj.zerossl.hmackey + } + }); + } else { + obj.client = new acme.Client({ + directoryUrl: obj.runAsProduction ? acme.directory.letsencrypt.production : acme.directory.letsencrypt.staging, + accountKey: accountKey + }); + } // Create Certificate Request (CSR) obj.log("Creating certificate request..."); var certRequest = { commonName: obj.leDomains[0] }; if (obj.leDomains.length > 1) { certRequest.altNames = obj.leDomains; } acme.forge.createCsr(certRequest).then(function (r) { - var csr = r[1]; + obj.csr = r[1]; obj.tempPrivateKey = r[0]; - obj.log("Requesting certificate from Let's Encrypt..."); + if(obj.zerossl) { obj.log("Requesting certificate from ZeroSSL..."); } else { obj.log("Requesting certificate from Let's Encrypt..."); } obj.client.auto({ - csr, + csr: obj.csr, email: obj.parent.config.letsencrypt.email, termsOfServiceAgreed: true, skipChallengeVerification: (obj.parent.config.letsencrypt.skipchallengeverification === true), diff --git a/mcrec.js b/mcrec.js index 4e10d8d4..43966b88 100644 --- a/mcrec.js +++ b/mcrec.js @@ -321,7 +321,7 @@ function setup() { InstallModules(['image-size'], start); } function start() { startEx(process.argv); } function startEx(argv) { 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(""); log(" Usage: node mcrec [file]"); diff --git a/meshagent.js b/meshagent.js index b3ce7654..7169b9ff 100644 --- a/meshagent.js +++ b/meshagent.js @@ -58,15 +58,15 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { dataAccounting(); if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket - if (arg == 2) { - try { + if (arg == 2) { + try { if (ws._socket._parent != null) ws._socket._parent.end(); else ws._socket.end(); - - if (obj.nodeid != null) { - parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); + + if (obj.nodeid != null) { + parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } @@ -616,7 +616,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { } 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)) { // Mesh name is hex instead of base64 const meshname = obj.meshid.substring(0, 18); @@ -1058,7 +1058,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) { if ((iplocs != null) && (iplocs.length == 1)) { // We have a location in the database for this remote IP - const iploc = nodes[0], x = {}; + const iploc = iplocs[0], x = {}; if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) { x.publicip = iploc.ip; x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000)); @@ -1067,10 +1067,10 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { } else { // Check if we need to ask for the IP location var doIpLocation = 0; - if (device.iploc == null) { + if (obj.iploc == null) { doIpLocation = 1; } else { - const loc = device.iploc.split(','); + const loc = obj.iploc.split(','); if (loc.length < 3) { doIpLocation = 2; } else { @@ -1924,6 +1924,10 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { if (!device.defender) { device.defender = {}; } if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; } } + if (command.lastbootuptime != null) { // Last Boot Up Time + if (!device.lastbootuptime) { device.lastbootuptime = ""; } + if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; } + } // Push Messaging Token if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) { @@ -1932,19 +1936,17 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { change = 1; // Don't save this change as an event to the db, so no log=1. parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included. } - + 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)) { // 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'); } // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match. } - // Volumes and BitLocker - if(command.volumes != null){ - if(!device.volumes) { device.volumes = {}; } - if (JSON.stringify(device.volumes) != JSON.stringify(command.volumes)) { /*changes.push('Volumes status');*/ device.volumes = command.volumes; change = 1; log = 1; } - } + // Remove old volumes and BitLocker data, this is part of sysinfo. + delete device.volumes; // If there are changes, event the new device if (change == 1) { diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 6d7969dc..91cec3b8 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -94,9 +94,41 @@ } }, "sqlite3": { - "type": "boolean", + "type": [ "boolean", "string", "object" ], "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": { "type": "object", @@ -446,9 +478,12 @@ "description": "By default, a nice looking 404 error page is displayed when needed. Set this to false to disable it." }, "selfUpdate": { - "type": "boolean", + "type": [ + "string", + "boolean" + ], "default": false, - "description": "When true, this server will attempt to self-update everyday after midnight." + "description": "When set to true, the server will check for a new version and attempt to self-update automatically a bit after midnight local time every day. If set to a specific version such as \"1.1.21\" the server will immediately update to the specified version on startup if it's not already at this version." }, "cleanNpmCacheOnUpdate": { "type": "boolean", @@ -485,11 +520,6 @@ "default": true, "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": { "type": "integer", "minimum": 1, @@ -605,7 +635,7 @@ "array" ], "default": null, - "description": "When set, only users from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, only users from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:userAllowedIP.txt\"" }, "userBlockedIP": { "type": [ @@ -613,7 +643,7 @@ "array" ], "default": null, - "description": "When set, users from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, users from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:userBlockedIP.txt\"" }, "agentAllowedIP": { "type": [ @@ -621,7 +651,7 @@ "array" ], "default": null, - "description": "When set, only agents from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, only agents from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:agentAllowedIP.txt\"" }, "agentBlockedIP": { "type": [ @@ -629,7 +659,7 @@ "array" ], "default": null, - "description": "When set, agents from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, agents from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:agentBlockedIP.txt\"" }, "authLog": { "type": "string", @@ -688,6 +718,19 @@ "default": false, "description": "When true, indicates that a TLS offloader is in front of the MeshCentral server. More typically, set this to the IP address of the reverse proxy or TLS offloader so that IP forwarding headers will be trusted. For example: \"127.0.0.1,192.168.1.100\"." }, + "useNodeDefaultTLSCiphers": { + "type": "boolean", + "default": false, + "description": "When true, get the default TLS ciphers from the node process, rather than using the recommended suites set up by meshcentral" + }, + "tlsCiphers": { + "type": [ + "string", + "array" + ], + "default": null, + "description": "Allows user to override the TLS ciphers used by meshcentral by default. If a string, should be a ':' separated list of ciphers to accept. If an array, should be an array of strings representing the ciphers to accept." + }, "trustedProxy": { "type": "string", "default": null, @@ -724,13 +767,27 @@ "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." }, + "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": { "type": "boolean", "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": { "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": { "type": "string", @@ -751,7 +808,7 @@ }, "webrtcConfig": { "type": "object", - "description": "The STUN servers used for WebRTC, if not specified the Google and Mozilla servers and used when the server is not in LAN mode.", + "description": "The STUN/TURN servers used for WebRTC, if not specified the Google and Cloudflare STUN servers are used when the server is not in LAN mode.", "properties": { "iceServers": { "type": "array", @@ -760,7 +817,8 @@ "type": "object", "properties": { "urls": { - "type": "string" + "type": "string", + "description": "STUN/TURN URLs, examples are stun:stun3.l.google.com:19302 or turn:openrelay.metered.ca:443" } }, "required": [ @@ -802,25 +860,82 @@ ] }, "autoBackup": { - "type": "object", + "type": [ + "boolean", + "object" + ], + "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": { "mongoDumpPath": { - "type": "string" + "type": "string", + "default": "mongodump", + "description": "The file path of where \"mongodump\" is located. Default is \"mongodump\"" }, "mysqlDumpPath": { - "type": "string" + "type": "string", + "default": "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": { - "type": "integer" + "type": "integer", + "default": 24, + "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": { - "type": "integer" + "type": "integer", + "default": 10, + "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": { - "type": "string" + "type": "string", + "default": "", + "minLength": 1, + "description": "When specified, the ZIP backups will be password protected with the zipPassword and the password cannot be a blank value" }, "backupPath": { - "type": "string" + "type": "string", + "default": "meshcentral-backups", + "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": { "type": "object", @@ -865,6 +980,54 @@ "description": "The maximum number of files to keep in the WebDAV folder, older files will be removed if needed." } } + }, + "s3": { + "type": "object", + "description": "Enabled automated upload of the server backups to an S3 server.", + "required": [ + "accessKey", + "secretKey", + "bucketName" + ], + "properties": { + "endpoint": { + "type": "string", + "default": "s3.amazonaws.com", + "description": "S3 Endpoint address e.g. myS3.myserver.com" + }, + "accessKey": { + "type": "string", + "description": "S3 accessKey, Required" + }, + "secretKey": { + "type": "string", + "description": "S3 secretKey, Required" + }, + "port": { + "type": "integer", + "default": 443, + "description": "S3 Endpoint port number, Default is 443" + }, + "ssl": { + "type": "boolean", + "default": true, + "description": "If \"true\", the S3 Endpoint will use \"https\", else it will use \"http\", Default is true" + }, + "bucketName": { + "type": "string", + "description": "S3 Bucket Name, Required" + }, + "folderName": { + "type": "string", + "default": "MeshCentral-Backups", + "description": "The name of the folder to create in the S3 Bucket. Defaults to \"MeshCentral-Backups\"." + }, + "maxFiles": { + "type": "integer", + "default": 0, + "description": "The maximum number of files to keep in the S3 folder, older files will be removed if needed. Default is 0 which is keep everything." + } + } } } }, @@ -971,6 +1134,24 @@ "required": [ "enabled" ] + }, + "watchdog": { + "type": "object", + "required": [ + "interval", + "timeout" + ], + "description": "This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. You MUST specify \"interval\" and \"timeout\".", + "properties": { + "interval": { + "type": "number", + "description": " This will check every X ms" + }, + "timeout": { + "type": "number", + "description": " If the timer is more than X ms late, it will warn to console AND mesherrors.txt" + } + } } } }, @@ -992,6 +1173,11 @@ "default": 2, "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": { "type": "string", "default": "MeshCentral", @@ -1012,6 +1198,11 @@ "default": null, "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": { "type": "string", "default": null, @@ -1022,6 +1213,11 @@ "default": true, "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": { "type": "integer", "default": null, @@ -1121,6 +1317,18 @@ "type": "string" } }, + "allowedOrigin": { + "type": [ + "array", + "boolean" + ], + "default": false, + "uniqueItems": true, + "description": "A list of allowed hostnames for HTTP request origin header. If false, a default list is created, if true, all hostnames are allowed.", + "items": { + "type": "string" + } + }, "welcomeText": { "type": "string", "description": "Text that will be shown on the login screen." @@ -1162,11 +1370,11 @@ "allowSavingDeviceCredentials": { "type": "boolean", "default": true, - "description": "Allow users to save SSH, RDP, VNC device credentials on the server that can be used by any other user." + "description": "Allow users to save SSH, RDP, VNC device credentials in the server." }, "trustedCert": { "type": "boolean", - "default": "This value is normally auto-detected, when set to true, MeshCentral assumes that the TLS certificate comes from a trusted CA and will insure download tools perform certificate checking." + "default": "This value is normally auto-detected, when set to true, MeshCentral assumes that the TLS certificate comes from a trusted CA and will ensure download tools perform certificate checking." }, "guestDeviceSharing": { "type": [ @@ -1474,6 +1682,11 @@ "default": true, "description": "Set to false to disable SMS 2FA." }, + "duo2factor": { + "type": "boolean", + "default": true, + "description": "Set to false to disable Duo 2FA." + }, "push2factor": { "type": "boolean", "default": true, @@ -1660,6 +1873,11 @@ "default": false, "description": "When enabled, this will show the notes panel in the device view" }, + "userSessionsSort": { + "type": "string", + "default": "SessionId", + "description": "Arrange the Connect sessions offered when multiple Terminal Sessions are present by 'SessionId', 'StationName' or 'Username' with 'SessionId' as the default sorting criteria." + }, "agentInviteCodes": { "type": "boolean", "default": false, @@ -1701,6 +1919,11 @@ "default": false, "description": "Enables the geo-location feature and device location map in the user interface, this feature is not being worked on." }, + "ipLocation": { + "type": "boolean", + "default": false, + "description": "When enabled, the remote agents will submit there approximate location to MeshCentral, Use in combination with \"geoLocation\"." + }, "novnc": { "type": "boolean", "default": true, @@ -1708,7 +1931,7 @@ }, "mstsc": { "type": "boolean", - "default": false, + "default": true, "description": "When enabled, activates the built-in web-based RDP client." }, "ssh": { @@ -1748,6 +1971,16 @@ "type": "boolean", "default": false, "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." } } }, @@ -1916,7 +2149,7 @@ "array" ], "default": null, - "description": "When set, only users from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, only users from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:userAllowedIP.txt\"" }, "userBlockedIP": { "type": [ @@ -1924,7 +2157,7 @@ "array" ], "default": null, - "description": "When set, users from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, users from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:userBlockedIP.txt\"" }, "agentAllowedIP": { "type": [ @@ -1932,7 +2165,7 @@ "array" ], "default": null, - "description": "When set, only agents from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, only agents from allowed IP address ranges can connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:agentAllowedIP.txt\"" }, "agentBlockedIP": { "type": [ @@ -1940,13 +2173,18 @@ "array" ], "default": null, - "description": "When set, agents from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\"" + "description": "When set, agents from these denied IP address ranges will not be able to connect to the server. Example: \"192.168.2.100,192.168.1.0/24\" \"file:agentBlockedIP.txt\"" }, "userSessionIdleTimeout": { "type": "integer", "default": null, "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": { "type": "object", "description": "Use this section to require user consent for this domain.", @@ -2056,7 +2294,7 @@ "login" ], "default": "any", - "description": "Indicate what terminal options are available when the user clicks the right mouse button on the terminal connect button." + "description": "Indicates the default linux terminal thats used when the user clicks the terminal connect button and disables the right mouse button on the terminal connect button when this is set." }, "launchCommand": { "type": "object", @@ -2105,12 +2343,12 @@ "TlsConnections": { "type": "boolean", "default": true, - "description": "When set to false, MeshCentral will use TLS to connect to Intel AMT, this is not recommended." + "description": "When set to false, MeshCentral will NOT use TLS to connect to Intel AMT, this is not recommended." }, "TlsAcmActivation": { "type": "boolean", "default": false, - "description": "When set to false, MeshCentral will not attempt a TLS ACM activation on Intel AMT v14+" + "description": "When set to true, MeshCentral will attempt a TLS ACM activation on Intel AMT v14+" }, "AdminAccounts": { "description": "List of username and passwords to try when connecting to Intel AMT.", @@ -2429,7 +2667,10 @@ } } } - } + }, + "required": [ + "certs" + ] }, "amtAcmActivation": { "type": "object", @@ -2476,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." }, + "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": { "type": "object", "properties": { @@ -2554,7 +2815,8 @@ }, "filepath": { "type": "string", - "description": "The file path where recording files are kept." + "default": "meshcentral-recordings", + "description": "The file path where recording files are kept. The default is \"meshcentral-recordings\" which sits next to \"meshcentral-data\"." }, "index": { "type": "boolean", @@ -2632,7 +2894,7 @@ "name": { "type": "string", "format": "hostname", - "description": "Optional hostname of the client, this defaults to the hostname of the machine. This is useful for SMTP relays." + "description": "Optional hostname of the client, this defaults to the hostname of the machine. This is useful for SMTP relays. This can also be set to \"console\" for console output debugging." }, "host": { "type": "string", @@ -2643,18 +2905,30 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "description": "SMTP server port number." + "default": 587, + "description": "SMTP server port number. This defaults to 587 if \"tls\" is false or 465 if \"tls\" is true)" }, "from": { "type": "string", "format": "email", "description": "Email address used in the messages from field." }, + "user": { + "type": "string", + "description": "SMTP username." + }, + "pass": { + "type": "string", + "description": "SMTP password." + }, "tls": { - "type": "boolean" + "type": "boolean", + "default": false, + "description": "Set SMTP to use TLS on connections, the default is false" }, "auth": { "type": "object", + "description": "This is used for OAuth2 authentication", "properties": { "clientId": { "type": "string" @@ -2664,6 +2938,11 @@ }, "refreshToken": { "type": "string" + }, + "type": { + "type": "string", + "default": "login", + "description": "Setting this indicates the authetication type, 'login' as default or 'oauth2'" } }, "required": [ @@ -2690,7 +2969,10 @@ } }, "required": [ - "from" + "host", + "port", + "from", + "tls" ] }, "sendmail": { @@ -2831,41 +3113,6 @@ "clientsecret" ] }, - "reddit": { - "type": "object", - "properties": { - "callbackurl": { - "type": "string", - "format": "uri" - }, - "newAccounts": { - "type": "boolean", - "default": false - }, - "newAccountsUserGroups": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "clientid": { - "type": "string" - }, - "clientsecret": { - "type": "string" - }, - "logouturl": { - "type": "string", - "format": "uri", - "description": "Then set, the user will be redirected to this URL when hitting the logout link." - } - }, - "required": [ - "clientid", - "clientsecret" - ] - }, "azure": { "type": "object", "properties": { @@ -2997,93 +3244,436 @@ }, "oidc": { "type": "object", + "description": "Enables the use of OpenID Connect SSO", + "anyOf": [ + { + "required": [ + "client" + ] + }, + { + "required": [ + "client", + "custom" + ] + }, + { + "required": [ + "client", + "issuer" + ] + }, + { + "required": [ + "clientid", + "clientsecret", + "issuer" + ] + } + ], "properties": { + "newAccounts": { + "type": "boolean", + "description": "Enable the creation of new accounts based upon Idp Authorization", + "default": true + }, + "newAccountsUserGroups": { + "type": [ + "string", + "array" + ], + "description": "Add all new users to these static MeshCentral user groups. Use this if the new groups section does not work with your preset.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "newAccountsRights": { + "type": [ + "array", + "string" + ], + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "clientid": { + "type": "string", + "depreciated": true, + "description": "REPLACED WITH 'client.client_id'" + }, + "clientsecret": { + "type": "string", + "description": "REPLACED WITH 'client.client_secret'" + }, "authorizationURL": { "type": "string", "format": "uri", - "description": "If set, this will be used as the authorization URL. (If set tokenURL and userInfoURL need set also)" - }, - "callbackURL": { - "type": "string", - "format": "uri", - "description": "Required, this is the URL that your SSO provider sends auth approval to." - }, - "clientid": { - "type": "string" - }, - "clientsecret": { - "type": "string" - }, - "issuer": { - "type": "string", - "format": "uri", - "description": "Full URL of SSO portal" + "depreciated": true, + "description": "REPLACED WITH 'issuer.authorization_endpoint'" }, "tokenURL": { "type": "string", "format": "uri", - "description": "If set, this will be used as the token URL. (If set authorizationURL and userInfoURL need set also)" + "depreciated": true, + "description": "REPLACED WITH 'issuer.token_endpoint': If set, this will be used as the token URL." }, "userInfoURL": { "type": "string", "format": "uri", - "description": "If set, this will be used as the user info URL. (If set authorizationURL and tokenURL need set also)" + "depreciated": true, + "description": "REPLACED WITH 'issuer.userinfo_endpoint': If set, this will be used as the user info URL." + }, + "scope": { + "type": [ + "string", + "array" + ], + "depreciated": true, + "description": "REPLACED WITH 'custom.scope': A list of scopes to request from the issuer." + }, + "callbackURL": { + "type": "string", + "format": "uri", + "depreciated": true, + "description": "REPLACED WITH 'client.redirect_uri': The URI your IdP sends you back to after successful authorization. This must match what is listed with your IdP." }, "logouturl": { "type": "string", "format": "uri", - "description": "Then set, the user will be redirected to this URL when hitting the logout link." + "description": "Overrides defaults ( [issuer.end_session_endpoint]?post_logout_redirect_uri=[post_logout_redirect_uri] OR [issuer.end_session_endpoint] )" }, - "newAccounts": { - "type": "boolean", - "default": true + "client": { + "type": "object", + "description": "OIDC Client Options", + "properties": { + "client_id": { + "type": "string", + "description": "REQUIRED: The client ID provided by your Identity Provider (IdP)" + }, + "client_secret": { + "type": "string", + "description": "REQUIRED: The client secret provided by your Identity Provider (IdP)" + }, + "id_token_signed_response_alg": { + "type": "string", + "default": "RS256", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "id_token_encrypted_response_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "id_token_encrypted_response_enc": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "userinfo_signed_response_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "userinfo_encrypted_response_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "userinfo_encrypted_response_enc": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "redirect_uri": { + "type": "string", + "format": "uri", + "description": "URI your IdP sends you after successful authorization. This must match what is listed with your IdP. (Default is https://[currentHost][currentPath]/auth-oidc-callback)" + }, + "response_types": { + "type": [ + "string", + "array" + ], + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details", + "default": [ + "code" + ] + }, + "post_logout_redirect_uri": { + "type": "string", + "format": "uri", + "description": "URI for your IdP to send you after logging out of IdP via MeshCentral. (Default is https:[currentHost][currentPath]/login)" + }, + "default_max_age": { + "type": "number", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "require_auth_time": { + "type": "boolean", + "default": false, + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "request_object_signing_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "request_object_encryption_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "request_object_encryption_enc": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "token_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ + "none", + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt" + ], + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "introspection_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ + "none", + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt" + ], + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "revocation_endpoint_auth_method": { + "type": "string", + "default": "client_secret_basic", + "enum": [ + "none", + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt" + ], + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "token_endpoint_auth_signing_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "introspection_endpoint_auth_signing_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "revocation_endpoint_auth_signing_alg": { + "type": "string", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + }, + "tls_client_certificate_bound_access_tokens": { + "type": "boolean", + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details" + } + }, + "required": [ + "client_id", + "client_secret" + ] + }, + "issuer": { + "type": [ + "string", + "object" + ], + "format": "uri", + "description": "Issuer options. Requires issuer URI (issuer.issuer) to discover missing information unless using preset", + "properties": { + "issuer": { + "type": "string", + "format": "uri", + "description": "URI of the issuer." + }, + "authorization_endpoint": { + "type": "string", + "format": "uri" + }, + "token_endpoint": { + "type": "string", + "format": "uri" + }, + "jwks_uri": { + "type": "string", + "format": "uri" + }, + "userinfo_endpoint": { + "type": "string", + "format": "uri" + }, + "revocation_endpoint": { + "type": "string", + "format": "uri" + }, + "introspection_endpoint": { + "type": "string", + "format": "uri" + }, + "end_session_endpoint": { + "type": "string", + "format": "uri", + "description": "URI to direct users to when logging out of MeshCentral. (Attempts to autodetect, defaults to '[issuer.issuer]/logout')" + }, + "registration_endpoint": { + "type": "string", + "format": "uri" + }, + "token_endpoint_auth_methods_supported": { + "type": "string" + }, + "token_endpoint_auth_signing_alg_values_supported": { + "type": "string" + }, + "introspection_endpoint_auth_methods_supported": { + "type": "string" + }, + "introspection_endpoint_auth_signing_alg_values_supported": { + "type": "string" + }, + "revocation_endpoint_auth_methods_supported": { + "type": "string" + }, + "revocation_endpoint_auth_signing_alg_values_supported": { + "type": "string" + }, + "request_object_signing_alg_values_supported": { + "type": "string" + }, + "mtls_endpoint_aliases": { + "type": "object", + "properties": { + "token_endpoint": { + "type": "string", + "format": "uri" + }, + "userinfo_endpoint": { + "type": "string", + "format": "uri" + }, + "revocation_endpoint": { + "type": "string", + "format": "uri" + }, + "introspection_endpoint": { + "type": "string", + "format": "uri" + } + } + } + } + }, + "custom": { + "type": "object", + "properties": { + "scope": { + "type": [ + "string", + "array" + ], + "description": "A list of scopes to request from the issuer.", + "default": "openid profile email", + "examples": [ + "openid", + [ + "openid", + "profile" + ], + "openid profile email", + "openid profile email groups" + ] + }, + "claims": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "preset": { + "type": "string", + "enum": [ + "azure", + "google" + ] + }, + "tenant_id": { + "type": "string", + "description": "REQUIRED FOR AZURE PRESET: Tenantid for Azure" + }, + "customer_id": { + "type": "string", + "description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)" + } + } }, "groups": { "type": "object", "properties": { + "recursive": { + "type": "boolean", + "default": false, + "description": "When true, the group memberships will be scanned recursively." + }, "required": { "type": [ - "string", "array" ], - "description": "When set, the user must be part of one of the OIDC user groups to login to MeshCentral." + "description": "Access is only granted to users who are a member of at least one of the listed required groups." }, "siteadmin": { "type": [ - "string", "array" ], - "description": "When set, users part of these groups will be promoted with site administrator in MeshCentral, users that are not part of these groups will be demoted." + "description": "Full site admin priviledges will be granted to users who are a member of at least one of the listed admin groups." + }, + "revokeAdmin": { + "type": "boolean", + "description": "If true, admin privileges will be revoked from users who are NOT a member of at least one of the listed admin groups." }, "sync": { "type": [ "boolean", "object" ], - "description": "Allows some or all ODIC user groups to be mirrored within MeshCentral as user groups.", + "default": false, + "description": "If true, all groups found during user login are mirrored into MeshCentral user groups.", "properties": { - "enabled": { - "type": "boolean", - "default": false - }, "filter": { "type": [ - "string", "array" ], - "description": "When set, limits what OIDC groups are mirrored into MeshCentral user groups." + "description": "Only groups listed here are mirrored into MeshCentral user groups." } } + }, + "scope": { + "type": "string", + "default": "groups", + "description": "Custom scope to use." + }, + "claim": { + "type": "string", + "default": "groups", + "description": "Custom claim to use." } } } - }, - "required": [ - "issuer", - "clientid", - "clientsecret", - "callbackURL" - ] + } } } }, @@ -3121,7 +3711,32 @@ "production": { "type": "boolean", "default": false, - "description": "By default a test certificate will be obtained from Let's Encrypt. Always start by getting a test certificate and make sure that works before setting this to true and obtaining a production certificate. Making too many bad requests for a production certificate will get you banned for a long period of time." + "description": "By default a test certificate will be obtained from Let's Encrypt. Setting \"zerossl\", will ignore this setting. Always start by getting a test certificate and make sure that works before setting this to true and obtaining a production certificate. Making too many bad requests for a production certificate will get you banned for a long period of time." + }, + "nochecks": { + "type": "boolean", + "default": false, + "description": "If you choose \"true\", MeshCentral won't verify if \"email\" is valid, has a valid MX record, AND if \"names\" doesn't contain a wildcard, can be resolved by DNS A/AAAA record." + }, + "zerossl": { + "type": "object", + "description": "If this object is set, we will use ZeroSSL for SSL creation instead of Let's Encrypt", + "required": [ + "kid", + "hmacKey" + ], + "properties": { + "kid": { + "type": "string", + "description": "EAB KID", + "default": "" + }, + "hmackey": { + "type": "string", + "description": "EAB HMAC KEY", + "default": "" + } + } } }, "required": [ @@ -3192,22 +3807,65 @@ "description": "Connects MeshCentral to a SMTP email server, allows MeshCentral to send email messages for 2FA or user notification.", "type": "object", "properties": { + "name": { + "type": "string", + "format": "hostname", + "description": "Optional hostname of the client, this defaults to the hostname of the machine. This is useful for SMTP relays. This can also be set to \"console\" for console output debugging." + }, "host": { "type": "string", - "format": "hostname" + "format": "hostname", + "description": "Hostname of the SMTP server." }, "port": { "type": "integer", "minimum": 1, - "maximum": 65535 + "maximum": 65535, + "default": 587, + "description": "SMTP server port number. This defaults to 587 if \"tls\" is false or 465 if \"tls\" is true)" }, "from": { "type": "string", "format": "email", "description": "Email address used in the messages from field." }, + "user": { + "type": "string", + "description": "SMTP username." + }, + "pass": { + "type": "string", + "description": "SMTP password." + }, "tls": { - "type": "boolean" + "type": "boolean", + "default": false, + "description": "Set SMTP to use TLS on connections, the default is false" + }, + "auth": { + "type": "object", + "description": "This is used for OAuth2 authentication", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "type": { + "type": "string", + "default": "login", + "description": "Setting this indicates the authetication type, 'login' as default or 'oauth2'" + } + }, + "required": [ + "clientId", + "clientSecret", + "refreshToken" + ] }, "tlscertcheck": { "type": "boolean" @@ -3219,6 +3877,11 @@ "type": "boolean", "default": true, "description": "When set to false, the email format and DNS MX record are not checked." + }, + "emailDelaySeconds": { + "type": "integer", + "default": 300, + "description": "Time to wait before sending a device connection/disconnection notification email. If many events occur, they will be merged into a single email." } }, "required": [ @@ -3228,6 +3891,36 @@ "tls" ] }, + "sendmail": { + "title": "Send email using the sendmail command", + "description": "Makes MeshCentral send emails using the Unix sendmail command. Allows MeshCentral to send email messages for 2FA or user notification.", + "type": "object", + "properties": { + "newline": { + "type": "string", + "default": "unix", + "description": "Possible values are unix or windows" + }, + "path": { + "type": "string", + "default": "sendmail", + "description": "Path to the sendmail command" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "default": null, + "description": "Array or arguments to pass to sendmail" + }, + "emailDelaySeconds": { + "type": "integer", + "default": 300, + "description": "Time to wait before sending a device connection/disconnection notification email. If many events occur, they will be merged into a single email." + } + } + }, "sms": { "title": "SMS provider", "description": "Connects MeshCentral to a SMS text messaging provider, allows MeshCentral to send SMS messages for 2FA or user notification.", @@ -3465,4 +4158,4 @@ } } } -} \ No newline at end of file +} diff --git a/meshcentral.js b/meshcentral.js index 40d22caf..a792dda5 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -139,9 +139,23 @@ function CreateMeshCentralServer(config, args) { try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not. // Check for invalid arguments - const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev']; + const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy']; for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } } + const ENVVAR_PREFIX = "meshcentral_" + let envArgs = [] + for (let [envvar, envval] of Object.entries(process.env)) { + if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) { + let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase() + if (!!argname && !(validArguments.indexOf(argname) == -1)) { + envArgs = envArgs.concat([`--${argname}`, envval]) + } + } + } + envArgs = require('minimist')(envArgs) + obj.args = Object.assign(envArgs, obj.args) if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; } + if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; } + if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; } for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence. if ((obj.args.help == true) || (obj.args['?'] == true)) { @@ -220,6 +234,12 @@ function CreateMeshCentralServer(config, args) { translateEngine.startEx(['', '', 'translateall', translationFile]); translateEngine.startEx(['', '', 'extractall', translationFile]); didSomething = true; + } else { + // Translate all of the default files + translateEngine.startEx(['', '', 'minifyall']); + translateEngine.startEx(['', '', 'translateall']); + translateEngine.startEx(['', '', 'extractall']); + didSomething = true; } // Check if "meshcentral-web" exists, if so, translate all pages in that folder. @@ -235,11 +255,34 @@ function CreateMeshCentralServer(config, args) { files = obj.fs.readdirSync(obj.webViewsOverridePath); for (var i in files) { var file = obj.path.join(obj.webViewsOverridePath, files[i]); - if (file.endsWith('.handlebars') || file.endsWith('-min.handlebars')) { + if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) { translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']); } } } + + // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder + for (i in obj.config.domains) { + if (i == "") continue; + var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views'); + if (require('fs').existsSync(path)) { + didSomething = true; + var files = obj.fs.readdirSync(path); + for (var a in files) { + var file = obj.path.join(path, files[a]); + if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) { + translateEngine.startEx(['', '', 'minify', file]); + } + } + files = obj.fs.readdirSync(path); + for (var a in files) { + var file = obj.path.join(path, files[a]); + if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) { + translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']); + } + } + } + } /* if (obj.webPublicOverridePath != null) { didSomething = true; @@ -254,6 +297,7 @@ function CreateMeshCentralServer(config, args) { */ if (didSomething == false) { console.log("Nothing to do."); } + console.log('Finished Translating.') process.exit(); return; } @@ -350,6 +394,92 @@ function CreateMeshCentralServer(config, args) { return; } } + + // 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 if (obj.args.indexmcrec != null) { @@ -453,8 +583,11 @@ function CreateMeshCentralServer(config, args) { // Launch MeshCentral as a child server and monitor it. obj.launchChildServer = function (startArgs) { 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.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) { if (childProcess.xrestart == 1) { setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart. @@ -526,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; } var datastr = data; 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) { var datastr = data; 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[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); } obj.logError(data); @@ -618,7 +751,7 @@ function CreateMeshCentralServer(config, args) { obj.performServerUpdate = function (version) { if (obj.serverSelfWriteAllowed != true) return false; if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); } - process.exit(200); + process.exit(200); return true; }; @@ -648,7 +781,7 @@ function CreateMeshCentralServer(config, args) { // Get new instance of the client const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token }); vault.unseal({ key: obj.args.vault.unsealkey }) - .then(function() { + .then(function () { if (obj.args.vaultdeleteconfigfiles) { vault.delete('secret/data/' + obj.args.vault.name) .then(function (r) { console.log('Done.'); process.exit(); }) @@ -764,7 +897,7 @@ function CreateMeshCentralServer(config, args) { } // Check top level configuration for any unrecognized values - if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [ i ]); } } } + if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [i]); } } } // Read IP lists from files if applicable config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip); @@ -774,11 +907,11 @@ function CreateMeshCentralServer(config, args) { config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip); // Check IP lists and ranges - if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(','); } } - if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(','); } } - if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(','); } } - if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(','); } } - if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(','); } } + if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } } + if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } } + if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } } + if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } } + if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } } if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; } if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); } if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); } @@ -867,10 +1000,10 @@ function CreateMeshCentralServer(config, args) { delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; // Disable 2FA delete user.msghandle; // Disable users 2fa messaging too var config = getConfig(false); - if(config.domains[user.domain].auth || config.domains[user.domain].authstrategies){ + if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) { console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one') obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); - }else{ + } else { if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) { // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password. var hashpasssplit = obj.args.hashpass.split(','); @@ -878,14 +1011,14 @@ function CreateMeshCentralServer(config, args) { user.salt = hashpasssplit[0]; user.hash = hashpasssplit[1]; obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; }); - } else if(obj.args.pass && (typeof obj.args.pass == 'string')) { + } else if (obj.args.pass && (typeof obj.args.pass == 'string')) { // Hash the password and reset the account. require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) { if (err) { console.log("Unable to reset password: " + err); process.exit(); return; } user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0); - }else{ + } else { console.log('Not setting a users password'); obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); } @@ -985,7 +1118,27 @@ function CreateMeshCentralServer(config, args) { // Show a list of all configuration files in the database if (obj.args.dblistconfigfiles) { - obj.db.GetAllType('cfile', function (err, docs) { if (err == null) { if (docs.length == 0) { console.log("No files found."); } else { for (var i in docs) { console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' bytes.'); } } } else { console.log('Unable to read from database.'); } process.exit(); }); return; + obj.db.GetAllType('cfile', function (err, docs) { + if (err == null) { + if (docs.length == 0) { + console.log("No files found."); + } else { + for (var i in docs) { + if (typeof obj.args.dblistconfigfiles == 'string') { + const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data); + if (data == null) { + console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.'); + } else { + console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.'); + } + } else { + console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.'); + } + } + } + } else { console.log('Unable to read from database.'); } process.exit(); + }); + return; } // Display the content of a configuration file in the database @@ -1030,7 +1183,11 @@ function CreateMeshCentralServer(config, args) { const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary'); console.log('Pushing ' + file + ', ' + binary.length + ' bytes.'); lockCount++; - obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } }); + if (obj.args.oldencrypt) { + obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } }); + } else { + obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } }); + } } } if (--lockCount == 0) { process.exit(); } @@ -1107,7 +1264,7 @@ function CreateMeshCentralServer(config, args) { for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } } //if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } } //if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } } - } + } //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it. obj.db.RemoveAll(function () { @@ -1194,7 +1351,7 @@ function CreateMeshCentralServer(config, args) { } // 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 if (obj.args.loadconfigfromdb) { @@ -1307,11 +1464,16 @@ function CreateMeshCentralServer(config, args) { if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; } if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; } if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; } - if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(','); } } - if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(','); } } - if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(','); } } - if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(','); } } + obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip); + obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip); + obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip); + obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip); + if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } } + if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } } + if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } } + if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } } if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } } + if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } } if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) { if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') { obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(','); @@ -1385,7 +1547,7 @@ function CreateMeshCentralServer(config, args) { if (i == '') { addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5); } else { - addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [ i ]); + addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]); } } } @@ -1446,8 +1608,18 @@ function CreateMeshCentralServer(config, args) { if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null; if (obj.args.redirport == null) obj.args.redirport = 80; if (obj.args.minifycore == null) obj.args.minifycore = false; - if (typeof args.agentidletimeout != 'number') { args.agentidletimeout = 150000; } else { args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec. - if ((obj.args.lanonly != true) && (obj.args.webrtconfig == null)) { obj.args.webrtconfig = { iceservers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.services.mozilla.com' }] }; } // Setup default WebRTC STUN servers + if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec. + if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig + obj.args.webrtcconfig = obj.args.webrtconfig; + delete obj.args.webrtconfig; + } + if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers + else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) { + if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers! + obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers; + delete obj.args.webrtcconfig.iceservers; + } + } if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } } // Setup a site administrator @@ -1487,7 +1659,7 @@ function CreateMeshCentralServer(config, args) { } // 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; }) } @@ -1823,9 +1995,17 @@ function CreateMeshCentralServer(config, args) { 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 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')) { // Setup the push messaging relay obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key); @@ -1834,8 +2014,12 @@ function CreateMeshCentralServer(config, args) { 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 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 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' }); @@ -1920,15 +2104,25 @@ function CreateMeshCentralServer(config, args) { obj.updateServerState('state', "running"); // Setup auto-backup defaults - if (obj.config.settings.autobackup == null) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; } - else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; } - - // Check that autobackup path is not within the "meshcentral-data" folder. - 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)))) { - addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21); - delete obj.config.settings.autobackup; + 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 == 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.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 if the database is capable of performing a backup + obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } }); + // Load Intel AMT passwords from the "amtactivation.log" file obj.loadAmtActivationLogPasswords(function (amtPasswords) { obj.amtPasswords = amtPasswords; @@ -2089,14 +2283,19 @@ function CreateMeshCentralServer(config, args) { // Check if we need to perform an automatic backup 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) { - if (err != null) return; + if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return} 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; } 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. obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database obj.db.performBackup(); // Perform the backup @@ -2155,7 +2354,7 @@ function CreateMeshCentralServer(config, args) { // Update the server state obj.updateServerState('state', "stopped"); }; - + // Event Dispatch obj.AddEventDispatch = function (ids, target) { obj.debug('dispatch', 'AddEventDispatch', ids); @@ -2218,6 +2417,10 @@ function CreateMeshCentralServer(config, args) { 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]; } } } + if (storeEvent.mesh) { + // Escape "mesh" names that may have "." and/or "$" + storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh); + } storeEvent.ids = ids; obj.db.StoreEvent(storeEvent); } @@ -2308,7 +2511,7 @@ function CreateMeshCentralServer(config, args) { // Event any changes on this server only if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) { - obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1 }); + obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() }); } } }; @@ -2477,7 +2680,7 @@ function CreateMeshCentralServer(config, args) { if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; } var powerState = 0, oldPowerState = state.powerState; if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; } - if ((state.powerState == null)|| (state.powerState == undefined) || (state.powerState != powerState)) { + if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) { state.powerState = powerState; eventConnectChange = 1; @@ -2489,7 +2692,7 @@ function CreateMeshCentralServer(config, args) { // Event the node connection change if (eventConnectChange == 1) { - obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1 }); + obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() }); // Save indication of node connection change const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType }; @@ -2521,7 +2724,7 @@ function CreateMeshCentralServer(config, args) { if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; } var powerState = 0, oldPowerState = state.powerState; if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; } - if ((state.powerState == null)|| (state.powerState == undefined) || (state.powerState != powerState)) { + if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) { state.powerState = powerState; eventConnectChange = 1; @@ -2593,7 +2796,7 @@ function CreateMeshCentralServer(config, args) { // Event the node connection change if (eventConnectChange == 1) { - obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1 }); + obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() }); // Notify any users of device disconnection obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo); @@ -2663,7 +2866,7 @@ function CreateMeshCentralServer(config, args) { if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) { meshcorePath = obj.path.join(__dirname, 'agents'); if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) { - obj.defaultMeshCores = obj.defaultMeshCoresHash = { }; if (func != null) { func(false); } // meshcore.js not found + obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found } } @@ -2729,7 +2932,7 @@ function CreateMeshCentralServer(config, args) { // We are adding a JS file to the meshcores var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3); if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files. - const moduleData = [ 'try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n' ]; + const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n']; // Merge this module // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms. @@ -2958,7 +3161,7 @@ function CreateMeshCentralServer(config, args) { } catch (ex) { } } }; - + // List of possible mesh agents obj.meshAgentsArchitectureNumbers = { 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, @@ -3051,13 +3254,13 @@ function CreateMeshCentralServer(config, args) { // Setup the time server var timeStampUrl = 'http://timestamp.comodoca.com/authenticode'; - if (args.agenttimestampserver === false) { timeStampUrl = null; } - else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; } + if (obj.args.agenttimestampserver === false) { timeStampUrl = null; } + else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; } // Setup the time server proxy var timeStampProxy = null; - if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; } - else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; } + if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; } + else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; } // Setup the pending operations counter var pendingOperations = 1; @@ -3204,7 +3407,7 @@ function CreateMeshCentralServer(config, args) { console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname)); } else { // Failed to sign agent - addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [ agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err ]); + addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]); } if (--pendingOperations === 0) { agentSignedFunc.func(); } } @@ -3542,7 +3745,7 @@ function CreateMeshCentralServer(config, args) { try { const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv); const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]); - return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64'); + return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { return null; } } @@ -3551,7 +3754,7 @@ function CreateMeshCentralServer(config, args) { if ((typeof data != 'string') || (data.length < 13)) return {}; if (key == null) { key = obj.loginCookieEncryptionKey; } try { - const buf = Buffer.from(data, 'base64'); + const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64'); const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12)); decipher.setAuthTag(buf.slice(12, 28)); return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8')); @@ -3673,7 +3876,7 @@ function CreateMeshCentralServer(config, args) { // Send event to log file if (obj.config.settings && obj.config.settings.log) { 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(); if (obj.xxLogFile == null) { try { @@ -3685,7 +3888,8 @@ function CreateMeshCentralServer(config, args) { if (obj.xxLogFile != null) { try { 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) { } } } @@ -3729,7 +3933,7 @@ function CreateMeshCentralServer(config, args) { function readIpListFromFile(arg) { if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg; var lines = null; - try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split('\r\n').join('\r').split('\r'); } catch (ex) { } + try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { } if (lines == null) return null; const validLines = []; for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); } @@ -3742,6 +3946,7 @@ function CreateMeshCentralServer(config, args) { 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); } 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); } } // auth.log functions @@ -3757,9 +3962,9 @@ function CreateMeshCentralServer(config, args) { if (obj.authlogfile != null) { // Write authlog to file try { const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()]; - msg = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n'); - obj.fs.write(obj.authlogfile, msg, function (err, written, string) { }); - } catch (ex) { console.log(ex); } + str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n'); + obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } }); + } catch (ex) { console.error(ex); } } } @@ -3780,7 +3985,7 @@ function CreateMeshCentralServer(config, args) { function checkResolveAll(names, func) { const dns = require('dns'), state = { func: func, count: names.length, err: null }; for (var i in names) { - dns.resolve(names[i], function (err, records) { + dns.lookup(names[i], { all: true }, function (err, records) { if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } } if (--this.state.count == 0) { this.state.func(this.state.err); } }.bind({ name: names[i], state: state })) @@ -3839,14 +4044,31 @@ function InstallModules(modules, args, func) { for (var i in modules) { // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require const moduleNameAndVersion = modules[i]; - const moduleInfo = moduleNameAndVersion.split('@', 2); - var moduleName = moduleInfo[0]; - var moduleVersion = moduleInfo[1]; - if (moduleName == '') { moduleName = moduleNameAndVersion; moduleVersion = null; } // If the module name starts with @, don't use @ as a version seperator. + const moduleInfo = moduleNameAndVersion.split('@', 3); + var moduleName = null; + var moduleVersion = null; + if(moduleInfo.length == 1){ // normal package without version + moduleName = moduleInfo[0]; + } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version + moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0]; + moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1]; + } else if (moduleInfo.length == 3) { // custom repo package and package with a version + moduleName = "@" + moduleInfo[1]; + moduleVersion = moduleInfo[2]; + } try { // Does the module need a specific version? 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 { // For all other modules, do the check here. // Is the module in package.json? Install exact version. @@ -3857,7 +4079,7 @@ function InstallModules(modules, args, func) { missingModules.push(moduleNameAndVersion); } } - + if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); } } } @@ -3895,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(); }); // Add a server warning, warnings will be shown to the administrator on the web application +// TODO: migrate to obj.addServerWarning? const serverWarnings = []; function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } } @@ -3925,7 +4148,8 @@ var ServerWarnings = { 23: "Unable to load agent icon file: {0}.", 24: "Unable to load agent logo file: {0}.", 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." }; */ @@ -3935,8 +4159,8 @@ var meshserver = null; var childProcess = null; var previouslyInstalledModules = {}; function mainStart() { - // Check the NodeJS is version 10 or better. - if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 11) { console.log("MeshCentral requires Node v11 or above, current version is " + process.version + "."); return; } + // Check the NodeJS is version 16 or better. + if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; } // If running within the node_modules folder, move working directory to the parent of the node_modules folder. if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); } @@ -3979,7 +4203,7 @@ function mainStart() { // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used var sspi = false; var ldap = false; - var passport = null; + var passport = []; var allsspi = true; var yubikey = false; var ssh = false; @@ -4000,59 +4224,70 @@ function mainStart() { if (mstsc == false) { config.domains[i].mstsc = false; } if (config.domains[i].ssh == true) { ssh = true; } if ((typeof config.domains[i].authstrategies == 'object')) { - if (passport == null) { passport = ['passport']; } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 + 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.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.reddit == 'object') && (typeof config.domains[i].authstrategies.reddit.clientid == 'string') && (typeof config.domains[i].authstrategies.reddit.clientsecret == 'string') && (passport.indexOf('passport-reddit') == -1)) { passport.push('passport-reddit'); } 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') && (typeof config.domains[i].authstrategies.oidc.clientid == 'string') && (typeof config.domains[i].authstrategies.oidc.clientsecret == 'string') && (typeof config.domains[i].authstrategies.oidc.issuer == 'string') && (passport.indexOf('@mstrhakr/passport-openidconnect') == -1)) { - if ((nodeVersion >= 17) || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13)) || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15)) || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) { passport.push('@mstrhakr/passport-openidconnect'); passport.push('openid-client'); passport.push('connect-flash'); } else { addServerWarning('This NodeJS version does not support OpenID.', 25); delete config.domains[i].authstrategies.oidc; } + if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) { + if ((nodeVersion >= 17) + || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13)) + || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15)) + || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) { + passport.push('openid-client@5.7.1'); + } else { + addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25); + delete config.domains[i].authstrategies.oidc; + } } if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); } } 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].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 // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile - var modules = ['archiver@5.3.2','body-parser@1.20.2','cbor@5.2.0','compression@1.7.4','cookie-session@2.0.0','express@4.18.2','express-handlebars@5.3.5','express-ws@4.0.0','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.14.2','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 (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 (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.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.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.mariadb != null) { modules.push('mariadb@3.2.2'); } // Add MariaDB, official 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.4.0'); } // Add MariaDB, 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.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 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 ((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 (config.settings.prometheus != null) { modules.push('prom-client'); } // Add Prometheus Metrics support if (typeof config.settings.autobackup == 'object') { // 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 if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); } // Enable WebDAV Support 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 + if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.2'); } } // Setup common password blocking @@ -4066,7 +4301,7 @@ function mainStart() { } // 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 if (config.sms != null) { @@ -4088,8 +4323,7 @@ function mainStart() { if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); } // Firebase Support - // Avoid 0.1.8 due to bugs: https://github.com/guness/node-xcs/issues/43 - if (config.firebase != null) { modules.push('node-xcs@0.1.7'); } + if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); } // Syslog support if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); } diff --git a/meshctrl.js b/meshctrl.js index 21ab0675..af4f1626 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -16,7 +16,7 @@ var settings = {}; const crypto = require('crypto'); const args = require('minimist')(process.argv.slice(2)); 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['_'].length == 0) { @@ -36,6 +36,8 @@ if (args['_'].length == 0) { console.log(" ListEvents - List server events."); console.log(" LoginTokens - List, create and remove login tokens."); 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(" RemoveDevice - Delete a device."); 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(" Upload - Upload a file to 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(" DeviceMessage - Open a message box 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; } 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': { 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]"); } @@ -238,10 +257,9 @@ if (args['_'].length == 0) { } case 'agentdownload': { if (args.type == null) { console.log(winRemoveSingleQuotes("Missing device type, use --type [agenttype]")); } - var at = parseInt(args.type); - if ((at == null) || isNaN(at) || (at < 1) || (at > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); } - if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); } - if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); } + else if ((parseInt(args.type) == null) || isNaN(parseInt(args.type)) || (parseInt(args.type) < 1) || (parseInt(args.type) > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); } + else if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); } + else if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); } else { ok = true; } break; } @@ -260,6 +278,12 @@ if (args['_'].length == 0) { else { ok = true; } 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': { 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."); } @@ -484,7 +508,7 @@ if (args['_'].length == 0) { console.log(" --realname [name] - Set the real name for this account."); console.log(" --phone [number] - Set the account phone number."); console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:"); - console.log(" manageusers,backup,restore,update,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents"); + console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices"); break; } case 'edituser': { @@ -501,7 +525,7 @@ if (args['_'].length == 0) { console.log(" --realname [name] - Set the real name for this account."); console.log(" --phone [number] - Set the account phone number."); console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:"); - console.log(" manageusers,backup,restore,update,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents"); + console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices"); break; } case 'removeuser': { @@ -789,6 +813,55 @@ if (args['_'].length == 0) { } 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': { console.log("Change information about a device, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'")); @@ -820,6 +893,7 @@ if (args['_'].length == 0) { console.log("Run a shell command on a remote device, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\"")); console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --powershell")); + console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --reply")); console.log("\r\nRequired arguments:\r\n"); if (process.platform == 'win32') { console.log(" --id [deviceid] - The device identifier."); @@ -831,6 +905,7 @@ if (args['_'].length == 0) { console.log(" --powershell - Run in Windows PowerShell."); console.log(" --runasuser - Attempt to run the command as logged in user."); console.log(" --runasuseronly - Only run the command as the logged in user."); + console.log(" --reply - Return with the output from running the command."); break; } case 'shell': { @@ -879,6 +954,7 @@ if (args['_'].length == 0) { console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30")); console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30 --daily")); console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type desktop,terminal --consent prompt")); + console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type http --port 80")); console.log("\r\nRequired arguments:\r\n"); if (process.platform == 'win32') { console.log(" --id [deviceid] - The device identifier."); @@ -886,21 +962,23 @@ if (args['_'].length == 0) { console.log(" --id '[deviceid]' - The device identifier."); } console.log("\r\nOptional arguments:\r\n"); - console.log(" --remove [shareid] - Remove a device sharing link."); - console.log(" --add [guestname] - Add a device sharing link."); - console.log(" --type [desktop,terminal,files] - Type of sharing to add, can be combined. default is desktop."); - console.log(" --viewonly - Make desktop sharing view only."); - console.log(" --consent [notify,prompt] - Consent flags, default is notify."); - console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now."); - console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time."); - console.log(" --duration [minutes] - Duration of the share, default is 60 minutes."); - console.log(" --daily - Add recurring daily device share."); - console.log(" --weekly - Add recurring weekly device share."); + console.log(" --remove [shareid] - Remove a device sharing link."); + console.log(" --add [guestname] - Add a device sharing link."); + console.log(" --type [desktop,terminal,files,http,https] - Type of sharing to add, can be combined. default is desktop."); + console.log(" --viewonly - Make desktop sharing view only."); + console.log(" --consent [notify,prompt,none] - Consent flags, default is notify."); + console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now."); + console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time."); + console.log(" --duration [minutes] - Duration of the share, default is 60 minutes."); + console.log(" --daily - Add recurring daily device share."); + console.log(" --weekly - Add recurring weekly device share."); + console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https."); break; } case 'agentdownload': { console.log("Download an agent of a specific type for a given device group, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3")); + console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3 --installflags 1")); console.log("\r\nRequired arguments:\r\n"); console.log(" --type [ArchitectureNumber] - Agent architecture number."); if (process.platform == 'win32') { @@ -908,6 +986,11 @@ if (args['_'].length == 0) { } else { console.log(" --id '[groupid]' - The device group identifier."); } + console.log("\r\nOptional arguments:\r\n"); + console.log(" --installflags [InstallFlagsNumber] - With the following choices:"); + console.log(" installflags 0 - Default, Interactive & Background, offers connect button & install/uninstall"); + console.log(" installflags 1 - Interactive only, offers only connect button, not install/uninstall"); + console.log(" installflags 2 - Background only, offers only install/uninstall, not connect"); break; } case 'upload': { @@ -939,6 +1022,21 @@ if (args['_'].length == 0) { console.log(" --target [localpath] - The local path to download the file to."); 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': { 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")); @@ -1113,7 +1211,7 @@ function performConfigOperations(args) { if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; } var config = null; try { config = fs.readFileSync(configFile).toString('utf8'); } catch (ex) { console.log("Error: Unable to read config.json"); return; } - try { config = require(configFile); } catch (e) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; } + try { config = JSON.parse(fs.readFileSync(configFile)); } catch (e) { console.log('ERROR: Unable to parse ' + configFile + '.'); return null; } if (args.adddomain != null) { didSomething++; if (config.domains == null) { config.domains = {}; } @@ -1261,10 +1359,10 @@ function serverConnect() { var domainid = '', username = 'admin'; if (args.logindomain != null) { domainid = args.logindomain; } if (args.loginuser != null) { username = args.loginuser; } - url += '?auth=' + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey); + url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey); } else { if (args.logindomain != null) { console.log("--logindomain can only be used along with --loginkey."); process.exit(); return; } - if (loginCookie != null) { url += '?auth=' + loginCookie; } + if (loginCookie != null) { url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + loginCookie; } } const ws = new WebSocket(url, options); @@ -1481,6 +1579,29 @@ function serverConnect() { ws.send(JSON.stringify(op)); 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': { var op = { action: 'editmesh', responseid: 'meshctrl' }; if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; } @@ -1619,7 +1740,9 @@ function serverConnect() { case 'runcommand': { var runAsUser = 0; if (args.runasuser) { runAsUser = 1; } else if (args.runasuseronly) { runAsUser = 2; } - ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser })); + var reply = false; + if (args.reply) { reply = true; } + ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser, reply: reply })); break; } case 'shell': @@ -1662,6 +1785,10 @@ function serverConnect() { var u = settings.xxurl.replace('wss://', 'https://').replace('/control.ashx', '/meshagents'); if (u.indexOf('?') > 0) { u += '&'; } else { u += '?'; } u += 'id=' + args.type + '&meshid=' + args.id; + if (args.installflags) { + if ((typeof parseInt(args.installflags) != 'number') || isNaN(parseInt(args.installflags)) || (parseInt(args.installflags) < 0) || (parseInt(args.installflags) > 2)) { console.log("Invalid Installflags."); process.exit(1); return; } + u += '&installflags=' + args.installflags; + } const options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer } const fs = require('fs'); const https = require('https'); @@ -1699,6 +1826,29 @@ function serverConnect() { req.end() 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': { if (args.add) { if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); } @@ -1707,10 +1857,12 @@ function serverConnect() { var p = 0; if (args.type != null) { var shareTypes = args.type.toLowerCase().split(','); - for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } } + for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files') && (shareTypes[i] != 'http') && (shareTypes[i] != 'https')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } } if (shareTypes.indexOf('terminal') >= 0) { p |= 1; } if (shareTypes.indexOf('desktop') >= 0) { p |= 2; } if (shareTypes.indexOf('files') >= 0) { p |= 4; } + if (shareTypes.indexOf('http') >= 0) { p |= 8; } + if (shareTypes.indexOf('https') >= 0) { p |= 16; } } if (p == 0) { p = 2; } // Desktop @@ -1745,6 +1897,19 @@ function serverConnect() { } } + var port = null; + // Set Port Number if http or https + if ((p & 8) || (p & 16)) { + 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 ((p & 8)) { + port = 80; + } else if ((p & 16)) { + port = 443; + } + } + // Start and end time var start = null, end = null; if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); } @@ -1761,14 +1926,14 @@ function serverConnect() { if ((typeof args.duration != 'number') || (args.duration < 1)) { console.log("Invalid duration value."); process.exit(1); return; } // Recurring sharing - ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, responseid: 'meshctrl' })); + ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, port: port, responseid: 'meshctrl' })); } else { if ((start == null) && (end == null)) { // Unlimited sharing - ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, responseid: 'meshctrl' })); + ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, port: port, responseid: 'meshctrl' })); } else { // Time limited sharing - ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, responseid: 'meshctrl' })); + ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, port: port, responseid: 'meshctrl' })); } } } else if (args.remove) { @@ -1845,11 +2010,11 @@ function serverConnect() { var srights = args.rights.toLowerCase().split(','); if (srights.indexOf('full') != -1) { siteadmin = 0xFFFFFFFF; } if (srights.indexOf('none') != -1) { siteadmin = 0x00000000; } - if (srights.indexOf('backup') != -1) { siteadmin |= 0x00000001; } + if (srights.indexOf('backup') != -1 || srights.indexOf('serverbackup') != -1) { siteadmin |= 0x00000001; } if (srights.indexOf('manageusers') != -1) { siteadmin |= 0x00000002; } - if (srights.indexOf('restore') != -1) { siteadmin |= 0x00000004; } + if (srights.indexOf('restore') != -1 || srights.indexOf('serverrestore') != -1) { siteadmin |= 0x00000004; } if (srights.indexOf('fileaccess') != -1) { siteadmin |= 0x00000008; } - if (srights.indexOf('update') != -1) { siteadmin |= 0x00000010; } + if (srights.indexOf('update') != -1 || srights.indexOf('serverupdate') != -1) { siteadmin |= 0x00000010; } if (srights.indexOf('locked') != -1) { siteadmin |= 0x00000020; } if (srights.indexOf('nonewgroups') != -1) { siteadmin |= 0x00000040; } if (srights.indexOf('notools') != -1) { siteadmin |= 0x00000080; } @@ -1857,6 +2022,7 @@ function serverConnect() { if (srights.indexOf('recordings') != -1) { siteadmin |= 0x00000200; } if (srights.indexOf('locksettings') != -1) { siteadmin |= 0x00000400; } if (srights.indexOf('allevents') != -1) { siteadmin |= 0x00000800; } + if (srights.indexOf('nonewdevices') != -1) { siteadmin |= 0x00001000; } } if (args.siteadmin) { siteadmin = 0xFFFFFFFF; } @@ -2053,6 +2219,8 @@ function serverConnect() { case 'toast': // TOAST case 'adduser': // ADDUSER case 'edituser': // EDITUSER + case 'addamtdevice': // ADDAMTDEVICE + case 'addlocaldevice': // ADDLOCALDEVICE case 'removedevices': // REMOVEDEVICE case 'changedevice': // EDITDEVICE case 'deleteuser': // REMOVEUSER @@ -2075,6 +2243,7 @@ function serverConnect() { case 'removeDeviceShare': case 'userbroadcast': { // BROADCAST 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 (data.responseid == 'meshctrl') { if (data.meshid) { console.log(data.result, data.meshid); } @@ -2085,6 +2254,7 @@ function serverConnect() { break; } case 'createDeviceShareLink': + case 'webrelay': if (data.result == 'OK') { if (data.publicid) { console.log('ID: ' + data.publicid); } console.log('URL: ' + data.url); @@ -2219,7 +2389,7 @@ function serverConnect() { if (args.filter != null) { for (var meshid in data.nodes) { for (var d in data.nodes[meshid]) { data.nodes[meshid][d].meshid = meshid; } - data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toLowerCase()); + data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toString().toLowerCase()); } } @@ -2370,6 +2540,8 @@ function serverConnect() { if (data.cause == 'noauth') { if (data.msg == 'tokenrequired') { console.log('Authentication token required, use --token [number].'); + } else if (data.msg == 'nokey') { + console.log('URL key is invalid or missing, please specify ?key=xxx in url'); } else { if ((args.loginkeyfile != null) || (args.loginkey != null)) { console.log('Invalid login, check the login key and that this computer has the correct time.'); @@ -2494,8 +2666,8 @@ function getDevicesThatMatchFilter(nodes, x) { } else if (tagSearch != null) { // Tag filter for (var d in nodes) { - if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(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; } } } + 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(nodes[d]); break; } } } } } else if (agentTagSearch != null) { // Agent Tag filter @@ -2733,7 +2905,7 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { } } } - if (node == null) { + if ((sysinfo == null && lastconnect == null && network == null) || (node == null)) { console.log("Invalid device id"); process.exit(); return; } diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js index b7888f9a..9b6d1e19 100644 --- a/meshdesktopmultiplex.js +++ b/meshdesktopmultiplex.js @@ -847,7 +847,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, id, func) { return; } // 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 firstBlock = JSON.stringify(metadata); 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.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; } 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.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; } diff --git a/meshmessaging.js b/meshmessaging.js index 47e1a6b4..646ae4a2 100644 --- a/meshmessaging.js +++ b/meshmessaging.js @@ -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); } }); } 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 headers = (typeof parent.config.messaging.ntfy.authorization == 'string') ? { 'Authorization': parent.config.messaging.ntfy.authorization } : {}; - const req = require('https').request(new URL(url), { method: 'POST', headers: headers }, function (res) { if (func != null) { func(true); } }); - req.on('error', function (err) { if (func != null) { func(false); } }); + const headers = { 'User-Agent': 'MeshCentral v' + parent.currentVer }; + if (typeof parent.config.messaging.ntfy.authorization == 'string') { headers['Authorization'] = parent.config.messaging.ntfy.authorization; } + const req = require('https').request(new URL(url), { method: 'POST', headers: headers }, function (res) { if (func != null) { func(res.statusCode == 200); } }); req.end(msg); } else if ((to.startsWith('zulip:')) && (obj.zulipClient != null)) { // zulip obj.zulipClient.sendMessage({ diff --git a/meshrelay.js b/meshrelay.js index 5164fdf4..7f91904d 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -119,6 +119,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { try { sr = parseInt(req.query.slowrelay); } catch (ex) { } 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 const MESHRIGHT_EDITMESH = 1; @@ -442,15 +445,15 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { relayinfo.peer1.sendPeerImage(); } else { // 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, userid: sessionUser._id, username: sessionUser.name, sessionid: obj.id, - ipaddr1: (obj.req == null) ? null : obj.req.clientIp, - ipaddr2: ((obj.peer == null) || (obj.peer.req == null)) ? null : obj.peer.req.clientIp, + ipaddr1: ((obj.peer == null) || (obj.peer.req == null)) ? null : obj.peer.req.clientIp, + ipaddr2: (obj.req == null) ? null : obj.req.clientIp, time: new Date().toLocaleString(), 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) @@ -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; } 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. - 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 (typeof domain.consentmessages == 'object') { 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.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; } 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.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. const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey); 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.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } 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.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; } 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.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)); 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) { - 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: {} }; - if (typeof domain.consentmessages == 'object') { + 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.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } 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.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 (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.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.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; } 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.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 == 12) { protocolStr = 'VNC'; } 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 }; 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); @@ -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 == 12) { protocolStr = 'VNC'; } else if (req.query.p == 13) { protocolStr = 'SSH-FILES'; } + else if (req.query.p == 14) { protocolStr = 'Web-TCP'; } 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 }; if (obj.guestname) { event.guestname = obj.guestname; } // If this is a sharing session, set the guest name here. diff --git a/meshuser.js b/meshuser.js index 8ce9dcdf..ac237dfd 100644 --- a/meshuser.js +++ b/meshuser.js @@ -84,7 +84,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use const PROTOCOL_WEBVNC = 204; // MeshCentral Satellite - const SATELLITE_PRESENT = 1; // This session is a MeshCentral Salellite session + const SATELLITE_PRESENT = 1; // This session is a MeshCentral Satellite session const SATELLITE_802_1x = 2; // This session supports 802.1x profile checking and creation // Events @@ -600,7 +600,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; } - if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) { serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); } + if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) {serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); } + if (typeof domain.logoutonidlesessiontimeout == 'boolean') { + serverinfo.logoutonidlesessiontimeout = domain.logoutonidlesessiontimeout; + } else { + // Default + serverinfo.logoutonidlesessiontimeout = true; + } if (user.siteadmin === SITERIGHT_ADMIN) { if (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) { serverinfo.manageAllDeviceGroups = true; } if (obj.crossDomain === true) { serverinfo.crossDomain = []; for (var i in parent.parent.config.domains) { serverinfo.crossDomain.push(i); } } @@ -778,10 +784,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Remove SSH credentials if present if (docs[i].ssh != null) { - if ((docs[i].ssh[obj.user._id] != null) && (docs[i].ssh[obj.user._id].u)) { - if (docs[i].ssh.k && docs[i].ssh[obj.user._id].kp) { docs[i].ssh = 2; } // Username, key and password - else if (docs[i].ssh[obj.user._id].k) { docs[i].ssh = 3; } // Username and key. No password. - else if (docs[i].ssh[obj.user._id].p) { docs[i].ssh = 1; } // Username and password + if ((docs[i].ssh[user._id] != null) && (docs[i].ssh[user._id].u)) { + if (docs[i].ssh.k && docs[i].ssh[user._id].kp) { docs[i].ssh = 2; } // Username, key and password + else if (docs[i].ssh[user._id].k) { docs[i].ssh = 3; } // Username and key. No password. + else if (docs[i].ssh[user._id].p) { docs[i].ssh = 1; } // Username and password else { delete docs[i].ssh; } } else { delete docs[i].ssh; @@ -789,7 +795,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } // Remove RDP credentials if present, only set to 1 if our userid has RDP credentials - if ((docs[i].rdp != null) && (docs[i].rdp[obj.user._id] != null)) { docs[i].rdp = 1; } else { delete docs[i].rdp; } + if ((docs[i].rdp != null) && (docs[i].rdp[user._id] != null)) { docs[i].rdp = 1; } else { delete docs[i].rdp; } // Remove Intel AMT credential if present if (docs[i].intelamt != null) { @@ -815,6 +821,14 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if ((xipkvmport != null) && (xipkvmport.sessions != null)) { docs[i].sessions = xipkvmport.sessions; } } + // Patch node links with names, like meshes links with names + for (var a in docs[i].links) { + if (!docs[i].links[a].name) { + if (parent.users[a] && parent.users[a].realname) { docs[i].links[a].name = parent.users[a].realname; } + else if (parent.users[a] && parent.users[a].name) { docs[i].links[a].name = parent.users[a].name; } + } + } + r[meshid].push(docs[i]); } const response = { action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag }; @@ -906,6 +920,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (common.IsFilenameValid(command.names[i]) === true) { var s = parent.path.join(scpath, command.names[i]), d = parent.path.join(path, command.names[i]); sendUpdate = false; + try { fs.mkdirSync(path); } catch (ex) { } // try to create folder first incase folder is missing copyFile(s, d, function (op) { if (op != null) { fs.unlink(op, function (err) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); }); } else { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } }, ((command.fileop == 'move') ? s : null)); } } @@ -913,7 +928,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Get a short file and send it back on the web socket if (common.validateString(command.file, 1, 4096) == false) return; const scpath = meshPathToRealPath(command.path, user); // This will also check access rights - if (scpath == null) break; + if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break; const filePath = parent.path.join(scpath, command.file); fs.stat(filePath, function (err, stat) { if ((err != null) || (stat == null) || (stat.size >= 204800)) return; @@ -928,7 +943,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (common.validateString(command.file, 1, 4096) == false) return; if (typeof command.data != 'string') return; const scpath = meshPathToRealPath(command.path, user); // This will also check access rights - if (scpath == null) break; + if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break; const filePath = parent.path.join(scpath, command.file); var data = null; try { data = Buffer.from(command.data, 'base64'); } catch (ex) { return; } @@ -988,6 +1003,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 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 (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.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; } @@ -1021,15 +1038,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // TODO: Add the meshes command.userid has access to (???) var filter = [command.userid]; + var actionfilter = null; + if (command.filter != null) { + if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter; + } + if ((command.limit == null) || (typeof command.limit != 'number')) { // Send the list of all events for this session - db.GetUserEvents(filter, domain.id, command.userid, function (err, docs) { + db.GetUserEvents(filter, domain.id, command.userid, actionfilter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { } }); } else { // Send the list of most recent events for this session, up to 'limit' count - db.GetUserEventsWithLimit(filter, domain.id, command.userid, command.limit, function (err, docs) { + db.GetUserEventsWithLimit(filter, domain.id, command.userid, command.limit, actionfilter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { } }); @@ -1047,15 +1069,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var limit = 10000; if (common.validateInt(command.limit, 1, 1000000) == true) { limit = command.limit; } + var filter = null; + if (command.filter != null) { + if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) filter = command.filter; + } + if (((rights & MESHRIGHT_LIMITEVENTS) != 0) && (rights != MESHRIGHT_ADMIN)) { // Send the list of most recent events for this nodeid that only apply to us, up to 'limit' count - db.GetNodeEventsSelfWithLimit(node._id, domain.id, user._id, limit, function (err, docs) { + db.GetNodeEventsSelfWithLimit(node._id, domain.id, user._id, limit, filter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { } }); } else { // Send the list of most recent events for this nodeid, up to 'limit' count - db.GetNodeEventsWithLimit(node._id, domain.id, limit, function (err, docs) { + db.GetNodeEventsWithLimit(node._id, domain.id, limit, filter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { } }); @@ -1075,15 +1102,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use for (var link in obj.user.links) { if (((obj.user.links[link].rights & MESHRIGHT_LIMITEVENTS) != 0) && ((obj.user.links[link].rights != MESHRIGHT_ADMIN))) { exGroupFilter2.push(link); } } for (var i in filter2) { if (exGroupFilter2.indexOf(filter2[i]) == -1) { filter.push(filter2[i]); } } + var actionfilter = null; + if (command.filter != null) { + if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter; + } + if ((command.limit == null) || (typeof command.limit != 'number')) { // Send the list of all events for this session - db.GetEvents(filter, domain.id, function (err, docs) { + db.GetEvents(filter, domain.id, actionfilter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { } }); } else { // Send the list of most recent events for this session, up to 'limit' count - db.GetEventsWithLimit(filter, domain.id, command.limit, function (err, docs) { + db.GetEventsWithLimit(filter, domain.id, command.limit, actionfilter, function (err, docs) { if (err != null) return; try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { } }); @@ -1100,7 +1132,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 1, tag: command.tag })); } catch (ex) { } return; } if ((command.limit == null) || (typeof command.limit != 'number')) { // Send the list of all recordings - db.GetEvents(['recording'], domain.id, function (err, docs) { + db.GetEvents(['recording'], domain.id, null, function (err, docs) { if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; } for (var i in docs) { delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these. @@ -1110,7 +1142,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); } else { // Send the list of most recent recordings, up to 'limit' count - db.GetEventsWithLimit(['recording'], domain.id, command.limit, function (err, docs) { + db.GetEventsWithLimit(['recording'], domain.id, command.limit, null, function (err, docs) { if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; } for (var i in docs) { delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these. @@ -2393,7 +2425,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } try { - if (common.validateString(command.userid, 8, 1024) == false) { err = "Invalid userid"; } // Check userid + if (common.validateString(command.userid, 1, 1024) == false) { err = "Invalid userid"; } // Check userid if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid if (command.userid.indexOf('/') == -1) { command.userid = 'user/' + domain.id + '/' + command.userid; } if (command.userid == obj.user._id) { err = "Can't remove self"; } // Can't add of modify self @@ -2530,77 +2562,98 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'addlocaldevice': { - if (common.validateString(command.meshid, 8, 134) == false) break; // Check meshid - if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain - if (common.validateString(command.devicename, 1, 256) == false) break; // Check device name - if (common.validateString(command.hostname, 1, 256) == false) break; // Check hostname - if (typeof command.type != 'number') break; // Type must be a number - if ((command.type != 4) && (command.type != 6) && (command.type != 29)) break; // Check device type - - // Get the mesh - mesh = parent.meshes[command.meshid]; - if (mesh) { - if (mesh.mtype != 3) return; // This operation is only allowed for mesh type 3, local device agentless mesh. - - // Check if this user has rights to do this - if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) return; - - // Create a new nodeid - parent.crypto.randomBytes(48, function (err, buf) { - // Create the new node - nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); - var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 3, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, agent: { id: command.type, caps: 0 } }; - db.Set(device); - - // Event the new node - parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id }); - }); + var err = null; + // Perform input validation + try { + if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid + if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name + if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname + if (typeof command.type != 'number') { err = "Invalid type"; } // Type must be a number + if ((command.type != 4) && (command.type != 6) && (command.type != 29)) { err = "Invalid type"; } // Check device type + else { + if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; } + mesh = parent.meshes[command.meshid]; + if (mesh == null) { err = "Unknown device group"; } + if (mesh.mtype != 3) { err = "Local device agentless mesh only allowed" } // This operation is only allowed for mesh type 3, local device agentless mesh. + else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; } + else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain + } + } catch (ex) { console.log(ex); err = "Validation exception: " + ex; } + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } } + break; } + // Create a new nodeid + parent.crypto.randomBytes(48, function (err, buf) { + // Create the new node + nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); + var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 3, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, agent: { id: command.type, caps: 0 } }; + db.Set(device); + + // Event the new node + parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id }); + // Send response if required + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addlocaldevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } } + }); break; } case 'addamtdevice': { if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added - if (common.validateString(command.meshid, 8, 134) == false) break; // Check meshid - if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain - if (common.validateString(command.devicename, 1, 256) == false) break; // Check device name - if (common.validateString(command.hostname, 1, 256) == false) break; // Check hostname - if (common.validateString(command.amtusername, 0, 16) == false) break; // Check username - if (common.validateString(command.amtpassword, 0, 16) == false) break; // Check password - if (command.amttls == '0') { command.amttls = 0; } else if (command.amttls == '1') { command.amttls = 1; } // Check TLS flag - if ((command.amttls != 1) && (command.amttls != 0)) break; + var err = null; + // Perform input validation + try { + if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid + if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name + if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname + if (common.validateString(command.amtusername, 0, 16) == false) { err = "Invalid amtusername"; } // Check username + if (common.validateString(command.amtpassword, 0, 16) == false) { err = "Invalid amtpassword"; } // Check password + if (command.amttls == '0') { command.amttls = 0; } else if (command.amttls == '1') { command.amttls = 1; } // Check TLS flag + if ((command.amttls != 1) && (command.amttls != 0)) { err = "Invalid amttls"; } + else { + if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; } + // Get the mesh + mesh = parent.meshes[command.meshid]; + if (mesh == null) { err = "Unknown device group"; } + if (mesh.mtype != 1) { err = "Intel AMT agentless mesh only allowed"; } // This operation is only allowed for mesh type 1, Intel AMT agentless mesh. + // Check if this user has rights to do this + else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; } + else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain + } + } catch (ex) { console.log(ex); err = "Validation exception: " + ex; } + + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } // If we are in WAN-only mode, hostname is not used if ((args.wanonly == true) && (command.hostname)) { delete command.hostname; } - // Get the mesh - mesh = parent.meshes[command.meshid]; - if (mesh) { - if (mesh.mtype != 1) return; // This operation is only allowed for mesh type 1, Intel AMT agentless mesh. + // Create a new nodeid + parent.crypto.randomBytes(48, function (err, buf) { + // Create the new node + nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); + var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 1, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: command.amttls } }; - // Check if this user has rights to do this - if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) return; + // Add optional feilds + if (common.validateInt(command.state, 0, 3)) { device.intelamt.state = command.state; } + if (common.validateString(command.ver, 1, 16)) { device.intelamt.ver = command.ver; } + if (common.validateString(command.hash, 1, 256)) { device.intelamt.hash = command.hash; } + if (common.validateString(command.realm, 1, 256)) { device.intelamt.realm = command.realm; } - // Create a new nodeid - parent.crypto.randomBytes(48, function (err, buf) { - // Create the new node - nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); - var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 1, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: command.amttls } }; + // Save the device to the database + db.Set(device); - // Add optional feilds - if (common.validateInt(command.state, 0, 3)) { device.intelamt.state = command.state; } - if (common.validateString(command.ver, 1, 16)) { device.intelamt.ver = command.ver; } - if (common.validateString(command.hash, 1, 256)) { device.intelamt.hash = command.hash; } - if (common.validateString(command.realm, 1, 256)) { device.intelamt.realm = command.realm; } + // Event the new node + parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id }); + // Send response if required + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addamtdevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } } + }); - // Save the device to the database - db.Set(device); - - // Event the new node - parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id }); - }); - } break; } case 'scanamtdevice': @@ -2902,6 +2955,48 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'webrelay': + { + if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid + else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain + else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present + else { + if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + var snode = command.nodeid.split('/'); + if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; } + } + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } + // Get the device rights + parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) { + // If node not found or we don't have remote control, reject. + if (node == null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } } + return; + } + var relayid = null; + var addr = null; + if (node.mtype == 3) { // Setup device relay if needed + var mesh = parent.meshes[node.meshid]; + if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; } + } + var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req); + var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0)); + if (webRelayPort == 0) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'WebRelay Disabled' })); return; } catch (ex) { } } + const authRelayCookie = parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey); + var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + command.nodeid + '&p=' + command.port + '&appid=' + command.appid + '&c=' + authRelayCookie; + if (addr != null) { url += '&addr=' + addr; } + if (relayid != null) { url += '&relayid=' + relayid } + command.url = url; + if (command.responseid != null) { command.result = 'OK'; } + try { ws.send(JSON.stringify(command)); } catch (ex) { } + }); + break; + } case 'runcommands': { if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's @@ -2942,13 +3037,19 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use return; } - // Send the commands to the agent - var agent = parent.wsagents[node._id]; - if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) { - try { agent.send(JSON.stringify({ action: 'msg', type: 'console', value: command.cmds, rights: rights, sessionid: ws.sessionId })); } catch (ex) { } + var theCommand = { action: 'msg', type: 'console', value: command.cmds, rights: rights, sessionid: ws.sessionId }; + if (parent.parent.multiServer != null) { // peering setup + parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand}); if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } } else { - if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } } + // Send the commands to the agent + var agent = parent.wsagents[node._id]; + if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) { + try { agent.send(JSON.stringify(theCommand)); } catch (ex) { } + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } } + } } } else { // This is a standard (bash/shell/powershell) command. @@ -2959,38 +3060,47 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use return; } - // Get the agent and run the commands - var agent = parent.wsagents[node._id]; - if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) { - // Check if this agent is correct for this command type - // command.type 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux/BSD/macOS - var commandsOk = false; - if ((agent.agentInfo.agentId > 0) && (agent.agentInfo.agentId < 5)) { - // Windows Agent - if ((command.type == 1) || (command.type == 2)) { commandsOk = true; } - else if (command.type === 0) { command.type = 1; commandsOk = true; } // Set the default type of this agent - } else { - // Non-Windows Agent - if (command.type == 3) { commandsOk = true; } - else if (command.type === 0) { command.type = 3; commandsOk = true; } // Set the default type of this agent - } - if (commandsOk == true) { + if (typeof command.reply != 'boolean') command.reply = false; + if (typeof command.responseid != 'string') command.responseid = null; + var msgid = 24; // "Running commands" + if (command.type == 1) { msgid = 99; } // "Running commands as user" + if (command.type == 2) { msgid = 100; } // "Running commands as user if possible" + // Check if this agent is correct for this command type + // command.type 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux/BSD/macOS + var commandsOk = false; + if ((node.agent.id > 0) && (node.agent.id < 5)) { + // Windows Agent + if ((command.type == 1) || (command.type == 2)) { commandsOk = true; } + else if (command.type === 0) { command.type = 1; commandsOk = true; } // Set the default type of this agent + } else { + // Non-Windows Agent + if (command.type == 3) { commandsOk = true; } + else if (command.type === 0) { command.type = 3; commandsOk = true; } // Set the default type of this agent + } + if (commandsOk == true) { + var theCommand = { action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser, reply: command.reply, responseid: command.responseid }; + var agent = parent.wsagents[node._id]; + if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) { // Send the commands to the agent - try { agent.send(JSON.stringify({ action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser })); } catch (ex) { } - if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } - + try { agent.send(JSON.stringify(theCommand)); } catch (ex) { } + if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } // Send out an event that these commands where run on this device var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]); - var msgid = 24; // "Running commands" - if (command.type == 1) { msgid = 99; } // "Running commands as user" - if (command.type == 2) { msgid = 100; } // "Running commands as user if possible" var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'runcommands', msg: 'Running commands', msgid: msgid, cmds: command.cmds, cmdType: command.type, runAsUser: command.runAsUser, domain: domain.id }; parent.parent.DispatchEvent(targets, obj, event); + } else if (parent.parent.multiServer != null) { // peering setup + // Send the commands to the agent + parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand}); + if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } + // Send out an event that these commands where run on this device + var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]); + var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'runcommands', msg: 'Running commands', msgid: msgid, cmds: command.cmds, cmdType: command.type, runAsUser: command.runAsUser, domain: domain.id }; + parent.parent.multiServer.DispatchEvent(targets, obj, event); } else { - if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } } + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } } } } else { - if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } } + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } } } } }); @@ -3103,8 +3213,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } if ((command.actiontype >= 300) && (command.actiontype < 400)) { - if ((command.actiontype != 302) && (command.actiontype != 308) && (command.actiontype < 310) && (command.actiontype > 312)) return; // Invalid action type. - // Intel AMT power command, actiontype: 2 = Power on, 8 = Power down, 10 = reset, 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL + if ((command.actiontype != 302) && (command.actiontype != 308) && (command.actiontype < 310) && (command.actiontype > 316)) return; // Invalid action type. + // Intel AMT power command, actiontype: 2 = Power on, 8 = Power down, 10 = reset, 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL, 15 = Power on to PXE, 16 = Reset to PXE parent.parent.DispatchEvent('*', obj, { action: 'amtpoweraction', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, actiontype: command.actiontype - 300 }); } else { if ((command.actiontype < 2) && (command.actiontype > 4)) return; // Invalid action type. @@ -3200,6 +3310,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } } return; } + node = common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials var mesh = parent.meshes[node.meshid], amtchange = 0; // Ready the node change event @@ -3521,6 +3632,41 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent(targets, obj, event); break; } + case 'otpduo': + { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + + // Do not allow if Duo is not supported + if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return; + + // Do not allow if Duo is disabled + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return; + + // Do not allow this command when logged in using a login token + if (req.session.loginToken != null) break; + + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here. + + // Check input + if ((typeof command.enabled != 'boolean') || (command.enabled != false)) return; + + // See if we really need to change the state + if ((command.enabled === false) && (user.otpduo == null)) return; + + // Change the duo 2FA of this user + delete user.otpduo; + parent.db.SetUser(user); + ws.send(JSON.stringify({ action: 'otpduo', success: true, enabled: command.enabled })); // Report success + + // Notify change + var targets = ['*', 'server-users', user._id]; + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } + var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: command.enabled ? 160 : 161, msg: command.enabled ? "Enabled duo two-factor authentication." : "Disabled duo two-factor authentication.", domain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. + parent.parent.DispatchEvent(targets, obj, event); + break; + } case 'otpauth-request': { // Do not allow this command if 2FA's are locked @@ -4303,7 +4449,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } // If we have view only remote desktop rights, force view-only on the guest share. - if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { command.viewOnly = true; command.p = (command.p & 1); } + if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { command.viewOnly = true; } // Create cookie var publicid = getRandomPassword(), startTime = null, expireTime = null, duration = null; @@ -4606,6 +4752,16 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; } parent.parent.DispatchEvent(['*', user._id], obj, message); } + + if (parent.parent.pluginHandler != null) // If the plugin's are not supported, reject this command. + { + command.userid = user._id; + try { + for( var pluginName in parent.parent.pluginHandler.plugins) + if( typeof parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent === 'function' ) + parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent(command, obj); + } catch (ex) { console.log('Error loading plugin handler (' + ex + ')'); } + } break; } case 'serverBackup': { @@ -4776,7 +4932,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); } else { // Old way - db.GetUserEvents([user._id], domain.id, user._id, function (err, docs) { + db.GetUserEvents([user._id], domain.id, user._id, null, function (err, docs) { if (err != null) return; var e = []; for (var i in docs) { @@ -4802,7 +4958,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); } else { // Old way - db.GetUserEvents([command.userid], domain.id, user._id, function (err, docs) { + db.GetUserEvents([command.userid], domain.id, user._id, null, function (err, docs) { if (err != null) return; var e = []; for (var i in docs) { if ((docs[i].msgArgs) && (docs[i].userid == command.userid) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); } } @@ -4920,252 +5076,295 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use case 'getDeviceDetails': { if ((common.validateStrArray(command.nodeids, 1) == false) && (command.nodeids != null)) break; // Check nodeids if (common.validateString(command.type, 3, 4) == false) break; // Check type + + const links = parent.GetAllMeshIdWithRights(user); + const extraids = getUserExtraIds(); + db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) { + if (docs == null) return; + const ids = []; + if (command.nodeids != null) { + // Create a list of node ids and query them for last device connection time + for (var i in command.nodeids) { ids.push('lc' + command.nodeids[i]); } + } else { + // Create a list of node ids for this user and query them for last device connection time + for (var i in docs) { ids.push('lc' + docs[i]._id); } + } + db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) { + const lastConnects = {}; + if (docs != null) { for (var i in docs) { lastConnects[docs[i]._id] = docs[i]; } } - // Create a list of node ids and query them for last device connection time - const ids = [] - for (var i in command.nodeids) { ids.push('lc' + command.nodeids[i]); } - db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) { - const lastConnects = {}; - if (docs != null) { for (var i in docs) { lastConnects[docs[i]._id] = docs[i]; } } + getDeviceDetailedInfo(command.nodeids, command.type, function (results, type) { + for (var i = 0; i < results.length; i++) { + // Remove any device system and network information is we do not have details rights to this device + if ((parent.GetNodeRights(user, results[i].node.meshid, results[i].node._id) & MESHRIGHT_DEVICEDETAILS) == 0) { + delete results[i].sys; delete results[i].net; + } - getDeviceDetailedInfo(command.nodeids, command.type, function (results, type) { - for (var i = 0; i < results.length; i++) { - // Remove any device system and network information is we do not have details rights to this device - if ((parent.GetNodeRights(user, results[i].node.meshid, results[i].node._id) & MESHRIGHT_DEVICEDETAILS) == 0) { - delete results[i].sys; delete results[i].net; + // Merge any last connection information + const lc = lastConnects['lc' + results[i].node._id]; + if (lc != null) { delete lc._id; delete lc.type; delete lc.meshid; delete lc.domain; results[i].lastConnect = lc; } + + // Remove any connectivity and power state information, that should not be in the database anyway. + // TODO: Find why these are sometimes saved in the db. + if (results[i].node.conn != null) { delete results[i].node.conn; } + if (results[i].node.pwr != null) { delete results[i].node.pwr; } + if (results[i].node.agct != null) { delete results[i].node.agct; } + if (results[i].node.cict != null) { delete results[i].node.cict; } + + // Add the connection state + var state = parent.parent.GetConnectivityState(results[i].node._id); + if (state) { + results[i].node.conn = state.connectivity; + results[i].node.pwr = state.powerState; + if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[results[i].node._id]; if (agent != null) { results[i].node.agct = agent.connectTime; } } + + // Use the connection time of the CIRA/Relay connection + if ((state.connectivity & 2) != 0) { + var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(results[i].node._id, null, true); + if ((ciraConnection != null) && (ciraConnection.tag != null)) { results[i].node.cict = ciraConnection.tag.connectTime; } + } + } + } - // Merge any last connection information - const lc = lastConnects['lc' + results[i].node._id]; - if (lc != null) { delete lc._id; delete lc.type;; delete lc.meshid; delete lc.domain; results[i].lastConnect = lc; } - } + var output = null; + if (type == 'csv') { + try { + // Create the CSV file + output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,lastbootuptime,cpu,osbuild,biosDate,biosVendor,biosVersion,biosSerial,biosMode,boardName,boardVendor,boardVersion,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n'; + for (var i = 0; i < results.length; i++) { + const nodeinfo = results[i]; - var output = null; - if (type == 'csv') { - try { - // Create the CSV file - output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,cpu,osbuild,biosDate,biosVendor,biosVersion,boardName,boardVendor,boardVersion,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n'; - for (var i = 0; i < results.length; i++) { - const nodeinfo = results[i]; - - // Node information - if (nodeinfo.node != null) { - const n = nodeinfo.node; - output += csvClean(n._id) + ',' + csvClean(n.name) + ',' + csvClean(n.rname ? n.rname : '') + ',' + csvClean(n.host ? n.host : '') + ',' + (n.icon ? n.icon : 1) + ',' + (n.ip ? n.ip : '') + ',' + (n.osdesc ? csvClean(n.osdesc) : '') + ',' + csvClean(parent.meshes[n.meshid].name); - if (typeof n.wsc == 'object') { - output += ',' + csvClean(n.wsc.antiVirus ? n.wsc.antiVirus : '') + ',' + csvClean(n.wsc.autoUpdate ? n.wsc.autoUpdate : '') + ',' + csvClean(n.wsc.firewall ? n.wsc.firewall : '') - } else { output += ',,,'; } - if (typeof n.volumes == 'object') { - var bitlockerdetails = '', firstbitlocker = true; - for (var a in n.volumes) { if (typeof n.volumes[a].protectionStatus !== 'undefined') { if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; } bitlockerdetails += a + '/' + n.volumes[a].volumeStatus; } } - output += ',' + csvClean(bitlockerdetails); - } else { - output += ','; - } - if (typeof n.av == 'object') { - var avdetails = '', firstav = true; - for (var a in n.av) { if (typeof n.av[a].product == 'string') { if (firstav) { firstav = false; } else { avdetails += '|'; } avdetails += (n.av[a].product + '/' + ((n.av[a].enabled) ? 'enabled' : 'disabled') + '/' + ((n.av[a].updated) ? 'updated' : 'notupdated')); } } - output += ',' + csvClean(avdetails); - } else { - output += ','; - } - if (typeof n.tags == 'object') { - var tagsdetails = '', firsttags = true; - for (var a in n.tags) { if (firsttags) { firsttags = false; } else { tagsdetails += '|'; } tagsdetails += n.tags[a]; } - output += ',' + csvClean(tagsdetails); - } else { - output += ','; - } - } else { - output += ',,,,,,,,,,,,,,,,,,,'; - } - - // System infomation - if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.windows)) { - // Windows - output += ','; - if (nodeinfo.sys.hardware.windows.cpu && (nodeinfo.sys.hardware.windows.cpu.length > 0) && (typeof nodeinfo.sys.hardware.windows.cpu[0].Name == 'string')) { output += csvClean(nodeinfo.sys.hardware.windows.cpu[0].Name); } - output += ','; - if (nodeinfo.sys.hardware.windows.osinfo && (nodeinfo.sys.hardware.windows.osinfo.BuildNumber)) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.BuildNumber); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_date)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_date); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_vendor); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_version); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_name); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_vendor); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_version); } - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.identifiers.product_uuid); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.windows.memory) { - var totalMemory = 0; - for (var j in nodeinfo.sys.hardware.windows.memory) { - if (nodeinfo.sys.hardware.windows.memory[j].Capacity) { - if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'number') { totalMemory += nodeinfo.sys.hardware.windows.memory[j].Capacity; } - if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.windows.memory[j].Capacity); } - } + // Node information + if (nodeinfo.node != null) { + const n = nodeinfo.node; + output += csvClean(n._id) + ',' + csvClean(n.name) + ',' + csvClean(n.rname ? n.rname : '') + ',' + csvClean(n.host ? n.host : '') + ',' + (n.icon ? n.icon : 1) + ',' + (n.ip ? n.ip : '') + ',' + (n.osdesc ? csvClean(n.osdesc) : '') + ',' + csvClean(parent.meshes[n.meshid].name); + if (typeof n.wsc == 'object') { + output += ',' + csvClean(n.wsc.antiVirus ? n.wsc.antiVirus : '') + ',' + csvClean(n.wsc.autoUpdate ? n.wsc.autoUpdate : '') + ',' + csvClean(n.wsc.firewall ? n.wsc.firewall : '') + } else { output += ',,,'; } + if (typeof n.volumes == 'object') { + var bitlockerdetails = '', firstbitlocker = true; + for (var a in n.volumes) { if (typeof n.volumes[a].protectionStatus !== 'undefined') { if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; } bitlockerdetails += a + '/' + n.volumes[a].volumeStatus; } } + output += ',' + csvClean(bitlockerdetails); + } else { + output += ','; } - output += csvClean('' + totalMemory); + if (typeof n.av == 'object') { + var avdetails = '', firstav = true; + for (var a in n.av) { if (typeof n.av[a].product == 'string') { if (firstav) { firstav = false; } else { avdetails += '|'; } avdetails += (n.av[a].product + '/' + ((n.av[a].enabled) ? 'enabled' : 'disabled') + '/' + ((n.av[a].updated) ? 'updated' : 'notupdated')); } } + output += ',' + csvClean(avdetails); + } else { + output += ','; + } + if (typeof n.tags == 'object') { + var tagsdetails = '', firsttags = true; + for (var a in n.tags) { if (firsttags) { firsttags = false; } else { tagsdetails += '|'; } tagsdetails += n.tags[a]; } + output += ',' + csvClean(tagsdetails); + } else { + output += ','; + } + if (typeof n.lastbootuptime == 'number') { output += ',' + n.lastbootuptime; } else { output += ','; } + } else { + output += ',,,,,,,,,,,,,,,,,,,,'; } - } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.mobile)) { - // Mobile - output += ','; - output += ','; - output += ','; - output += ','; - output += ','; - if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); } - output += ','; - if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.model)) { output += csvClean(nodeinfo.sys.hardware.mobile.model); } - output += ','; - if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.brand)) { output += csvClean(nodeinfo.sys.hardware.mobile.brand); } - output += ','; - output += ','; - if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.id)) { output += csvClean(nodeinfo.sys.hardware.mobile.id); } - output += ','; - output += ','; - output += ','; - output += ','; - output += ','; - output += ','; - output += ','; - } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.linux)) { - // Linux - output += ','; - if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.cpu_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.cpu_name); } - output += ',,'; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_date)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_date); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_vendor); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_version)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_version); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_name)) { output += csvClean(nodeinfo.sys.hardware.linux.board_name); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.board_vendor); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_version)) { output += csvClean(nodeinfo.sys.hardware.linux.board_version); } - output += ','; - if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.linux.product_uuid); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); } - output += ','; - if (nodeinfo.sys.hardware.linux.memory) { - if (nodeinfo.sys.hardware.linux.memory.Memory_Device) { + + // System infomation + if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.windows)) { + // Windows + output += ','; + if (nodeinfo.sys.hardware.windows.cpu && (nodeinfo.sys.hardware.windows.cpu.length > 0) && (typeof nodeinfo.sys.hardware.windows.cpu[0].Name == 'string')) { output += csvClean(nodeinfo.sys.hardware.windows.cpu[0].Name); } + output += ','; + if (nodeinfo.sys.hardware.windows.osinfo && (nodeinfo.sys.hardware.windows.osinfo.BuildNumber)) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.BuildNumber); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_date)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_date); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_vendor); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_version); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_serial); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_mode)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_mode); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_name); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_vendor); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_version); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.identifiers.product_uuid); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.windows.memory) { var totalMemory = 0; - for (var j in nodeinfo.sys.hardware.linux.memory.Memory_Device) { - if (nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size) { - if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'number') { totalMemory += nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size; } - if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size); } + for (var j in nodeinfo.sys.hardware.windows.memory) { + if (nodeinfo.sys.hardware.windows.memory[j].Capacity) { + if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'number') { totalMemory += nodeinfo.sys.hardware.windows.memory[j].Capacity; } + if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.windows.memory[j].Capacity); } } } - output += csvClean('' + (totalMemory * Math.pow(1024, 3))); + output += csvClean('' + totalMemory); } - } - } else { - output += ',,,,,,,,,,,,,,,,'; - } - - // Agent information - if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.agentvers)) { - output += ','; - if (nodeinfo.sys.hardware.agentvers.openssl) { output += csvClean(nodeinfo.sys.hardware.agentvers.openssl); } - output += ','; - if (nodeinfo.sys.hardware.agentvers.commitDate) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitDate); } - output += ','; - if (nodeinfo.sys.hardware.agentvers.commitHash) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitHash); } - output += ','; - if (nodeinfo.sys.hardware.agentvers.compileTime) { output += csvClean(nodeinfo.sys.hardware.agentvers.compileTime); } - } else { - output += ',,,,'; - } - - // Network interfaces - if ((nodeinfo.net) && (nodeinfo.net.netif2)) { - output += ','; - output += Object.keys(nodeinfo.net.netif2).length; // Interface count - var macs = [], addresses = []; - for (var j in nodeinfo.net.netif2) { - if (Array.isArray(nodeinfo.net.netif2[j])) { - for (var k = 0; k < nodeinfo.net.netif2[j].length; k++) { - if (typeof nodeinfo.net.netif2[j][k].mac == 'string') { macs.push(nodeinfo.net.netif2[j][k].mac); } - if (typeof nodeinfo.net.netif2[j][k].address == 'string') { addresses.push(nodeinfo.net.netif2[j][k].address); } + } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.mobile)) { + // Mobile + output += ','; + output += ','; + output += ','; + output += ','; + output += ','; + if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); } + output += ','; + output += ','; + output += ','; + if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.model)) { output += csvClean(nodeinfo.sys.hardware.mobile.model); } + output += ','; + if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.brand)) { output += csvClean(nodeinfo.sys.hardware.mobile.brand); } + output += ','; + output += ','; + if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.id)) { output += csvClean(nodeinfo.sys.hardware.mobile.id); } + output += ','; + output += ','; + output += ','; + output += ','; + output += ','; + output += ','; + output += ','; + } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.linux)) { + // Linux + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.cpu_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.cpu_name); } + output += ',,'; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_date)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_date); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_vendor); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_version)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_version); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_serial)) { output += csvClean(nodeinfo.sys.hardware.linux.product_serial); } + else if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_serial); } + output += ','; + if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_mode)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_mode); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_name)) { output += csvClean(nodeinfo.sys.hardware.linux.board_name); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.board_vendor); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_version)) { output += csvClean(nodeinfo.sys.hardware.linux.board_version); } + output += ','; + if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.linux.product_uuid); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); } + output += ','; + if (nodeinfo.sys.hardware.linux.memory) { + if (nodeinfo.sys.hardware.linux.memory.Memory_Device) { + var totalMemory = 0; + for (var j in nodeinfo.sys.hardware.linux.memory.Memory_Device) { + if (nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size) { + if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'number') { totalMemory += nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size; } + if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size); } + } + } + output += csvClean('' + (totalMemory * Math.pow(1024, 3))); } } + } else { + output += ',,,,,,,,,,,,,,,,,,'; } - output += ','; - output += csvClean(macs.join(' ')); // MACS - output += ','; - output += csvClean(addresses.join(' ')); // Addresses - } else { - output += ',,,'; - } - // Last connection information - if (nodeinfo.lastConnect) { - output += ','; - if (nodeinfo.lastConnect.time) { - // Last connection time - if ((typeof command.l == 'string') && (typeof command.tz == 'string')) { - output += csvClean(new Date(nodeinfo.lastConnect.time).toLocaleString(command.l, { timeZone: command.tz })) - } else { - output += nodeinfo.lastConnect.time; + // Agent information + if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.agentvers)) { + output += ','; + if (nodeinfo.sys.hardware.agentvers.openssl) { output += csvClean(nodeinfo.sys.hardware.agentvers.openssl); } + output += ','; + if (nodeinfo.sys.hardware.agentvers.commitDate) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitDate); } + output += ','; + if (nodeinfo.sys.hardware.agentvers.commitHash) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitHash); } + output += ','; + if (nodeinfo.sys.hardware.agentvers.compileTime) { output += csvClean(nodeinfo.sys.hardware.agentvers.compileTime); } + } else { + output += ',,,,'; + } + + // Network interfaces + if ((nodeinfo.net) && (nodeinfo.net.netif2)) { + output += ','; + output += Object.keys(nodeinfo.net.netif2).length; // Interface count + var macs = [], addresses = []; + for (var j in nodeinfo.net.netif2) { + if (Array.isArray(nodeinfo.net.netif2[j])) { + for (var k = 0; k < nodeinfo.net.netif2[j].length; k++) { + if (typeof nodeinfo.net.netif2[j][k].mac == 'string') { macs.push(nodeinfo.net.netif2[j][k].mac); } + if (typeof nodeinfo.net.netif2[j][k].address == 'string') { addresses.push(nodeinfo.net.netif2[j][k].address); } + } + } } + output += ','; + output += csvClean(macs.join(' ')); // MACS + output += ','; + output += csvClean(addresses.join(' ')); // Addresses + } else { + output += ',,,'; } - output += ','; - if (typeof nodeinfo.lastConnect.addr == 'string') { output += csvClean(nodeinfo.lastConnect.addr); } // Last connection address and port - } else { - output += ',,'; + + // Last connection information + if (nodeinfo.lastConnect) { + output += ','; + if (nodeinfo.lastConnect.time) { + // Last connection time + if ((typeof command.l == 'string') && (typeof command.tz == 'string')) { + output += csvClean(new Date(nodeinfo.lastConnect.time).toLocaleString(command.l, { timeZone: command.tz })) + } else { + output += nodeinfo.lastConnect.time; + } + } + output += ','; + if (typeof nodeinfo.lastConnect.addr == 'string') { output += csvClean(nodeinfo.lastConnect.addr); } // Last connection address and port + } else { + output += ',,'; + } + + output += '\r\n'; } + } catch (ex) { console.log(ex); } + } else { + // Create the JSON file - output += '\r\n'; + // Add the device group name to each device + for (var i = 0; i < results.length; i++) { + const nodeinfo = results[i]; + if (nodeinfo.node) { + const mesh = parent.meshes[nodeinfo.node.meshid]; + if (mesh) { results[i].node.groupname = mesh.name; } + } } - } catch (ex) { console.log(ex); } - } else { - // Create the JSON file - // Add the device group name to each device - for (var i = 0; i < results.length; i++) { - const nodeinfo = results[i]; - if (nodeinfo.node) { - const mesh = parent.meshes[nodeinfo.node.meshid]; - if (mesh) { results[i].node.groupname = mesh.name; } - } + output = JSON.stringify(results, null, 2); } - - output = JSON.stringify(results, null, 2); - } - try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { } + try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { } + }); }); }); - break; } case 'endDesktopMultiplex': { @@ -5414,7 +5613,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'heapdump': [serverUserCommandHeapDump, ""], 'heapdump2': [serverUserCommandHeapDump2, ""], 'help': [serverUserCommandHelp, ""], - 'info': [serverUserCommandInfo, "Returns the most immidiatly useful information about this server, including MeshCentral and NodeJS versions. This is often information required to file a bug."], + 'info': [serverUserCommandInfo, "Returns the most immidiatly useful information about this server, including MeshCentral and NodeJS versions. This is often information required to file a bug. Optionally use info h for human readable form."], 'le': [serverUserCommandLe, ""], 'lecheck': [serverUserCommandLeCheck, ""], 'leevents': [serverUserCommandLeEvents, ""], @@ -5451,7 +5650,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid else if (common.validateInt(command.rights) == false) { err = 'Invalid rights'; } // Device rights must be an integer else if ((command.rights & 7) != 0) { err = 'Invalid rights'; } // EDITMESH, MANAGEUSERS or MANAGECOMPUTERS rights can't be assigned to a user to device link - else if ((common.validateStrArray(command.usernames, 1, 64) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters + else if ((common.validateStrArray(command.usernames, 1, 128) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters else { if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain @@ -5598,7 +5797,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use try { if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid groupid'; } // Check the meshid else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer - else if ((common.validateStrArray(command.usernames, 1, 64) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters + else if ((common.validateStrArray(command.usernames, 1, 128) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters else { if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; } mesh = parent.meshes[command.meshid]; @@ -5755,13 +5954,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use return; } - for(var x in parent.users) { - if(parent.users[x].email==command.email){ - displayNotificationMessage("Email address already in use", "New Account", "ServerNotify"); - return; - } - } - // Check if we exceed the maximum number of user accounts db.isMaxType(newuserdomain.limits.maxuseraccounts, 'user', newuserdomain.id, function (maxExceed) { if (maxExceed) { @@ -5936,7 +6128,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use try { if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; } else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid - else if (common.validateStrArray(command.usernames, 1, 64) == false) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters + else if (common.validateStrArray(command.usernames, 1, 128) == false) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters else { var ugroupidsplit = command.ugrpid.split('/'); if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = 'Invalid groupid'; } @@ -6103,7 +6295,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.db.SetUser(user); // Event the change - var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, msgid: 2, msgArgs: [(oldlang ? oldlang : 'default'), (user.lang ? user.lang : 'default')] }; + var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, msgid: 3, msgArgs: ['', (oldlang ? oldlang : 'default'), (user.lang ? user.lang : 'default')] }; if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. message.msg = 'Changed language from ' + (oldlang ? oldlang : 'default') + ' to ' + (user.lang ? user.lang : 'default'); @@ -6224,6 +6416,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (command.nodeid) { cookieContent.nodeid = command.nodeid; } if (command.tcpaddr) { cookieContent.tcpaddr = command.tcpaddr; } // Indicates the browser want the agent to TCP connect to a remote address if (command.tcpport) { cookieContent.tcpport = command.tcpport; } // Indicates the browser want the agent to TCP connect to a remote port + if (command.tag == 'novnc') { cookieContent.p = 12; } // If tag is novnc we must encode a protocol for meshrelay logging if (node.mtype == 3) { cookieContent.lc = 1; command.localRelay = true; } // Indicate this is for a local connection command.cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey); command.trustedCert = parent.isTrustedCert(domain); @@ -6272,6 +6465,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use delete doc.type; delete doc.domain; delete doc._id; + + // If this is not a device group admin users, don't send any BitLocker recovery passwords + if ((rights != MESHRIGHT_ADMIN) && (doc.hardware) && (doc.hardware.windows) && (doc.hardware.windows.volumes)) { + for (var i in doc.hardware.windows.volumes) { delete doc.hardware.windows.volumes[i].recoveryPassword; } + } + if (command.nodeinfo === true) { doc.node = node; doc.rights = rights; } obj.send(doc); } else { @@ -6530,7 +6729,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use const manageAllDeviceGroups = ((user.siteadmin == 0xFFFFFFFF) && (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0)); if ((command.devGroup != null) && (manageAllDeviceGroups == false) && ((user.links == null) || (user.links[command.devGroup] == null))) return; // Asking for a device group that is not allowed - const msgIdFilter = [5, 10, 11, 12, 122, 123, 124, 125, 126]; + const msgIdFilter = [5, 10, 11, 12, 122, 123, 124, 125, 126, 144]; switch (command.type) { case 1: { remoteSessionReport(command, manageAllDeviceGroups, msgIdFilter); @@ -7072,13 +7271,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use for (var i in parent.badLoginTable) { badLoginCount++; if (typeof parent.badLoginTable[i] == 'number') { - cmdData.result += "Cooloff for " + Math.floor((parent.badLoginTable[i] - Date.now()) / 60000) + " minute(s)\r\n"; + cmdData.result += (i + " - Cooloff for " + Math.floor((parent.badLoginTable[i] - Date.now()) / 60000) + " minute(s)\r\n"); } else { - if (parent.badLoginTable[i].length > 1) { - cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " records\r\n"); - } else { - cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " record\r\n"); - } + cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " attempt(s) until Cooloff ban\r\n"); } } if (badLoginCount == 0) { cmdData.result += 'No bad logins.'; } @@ -7364,7 +7559,26 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } function serverUserCommandInfo(cmdData) { - var info = {}; + function convertSeconds (s, form) { + if (!['long', 'shortprecise'].includes(form)) { + form = 'shortprecise'; + } + let t = {}, r = ''; + t.d = Math.floor(s / (24 * 3600)); + s %= 24 * 3600; + t.h= Math.floor(s / 3600); + s %= 3600; + t.m = Math.floor(s / 60); + t.s =(s%60).toFixed(0); + if ( form == 'long') { + r = t.d + ((t.d == 1) ? ' day, ' : ' days, ') + t.h + ((t.h == 1) ? ' hour, ' : ' hours, ') + t.m + ((t.m == 1) ? ' minute, ' : ' minutes, ') + t.s+ ((t.s == 1) ? ' second' : ' seconds'); + } else if (form == 'shortprecise') { + r = String(t.d).padStart(2, '0') + ':' + String(t.h).padStart(2, '0') + ':' + String(t.m).padStart(2, '0') + ':' + String((s%60).toFixed(2)).padStart(5, '0') + 's'; + } + return r; + } + var info = {}, arg = null, t = {}, r = ''; + if ((cmdData.cmdargs['_'] != null) && (cmdData.cmdargs['_'][0] != null)) { arg = cmdData.cmdargs['_'][0].toLowerCase(); } try { info.meshVersion = 'v' + parent.parent.currentVer; } catch (ex) { } try { info.nodeVersion = process.version; } catch (ex) { } try { info.runMode = (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][(args.lanonly ? 2 : (args.wanonly ? 1 : 0))]); } catch (ex) { } @@ -7376,9 +7590,24 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use try { info.platform = process.platform; } catch (ex) { } try { info.arch = process.arch; } catch (ex) { } try { info.pid = process.pid; } catch (ex) { } - try { info.uptime = process.uptime(); } catch (ex) { } - try { info.cpuUsage = process.cpuUsage(); } catch (ex) { } - try { info.memoryUsage = process.memoryUsage(); } catch (ex) { } + if (arg == 'h') { + try { + info.uptime = convertSeconds(process.uptime(), 'long'); + info.cpuUsage = { + system: (convertSeconds(process.cpuUsage().system /1000000)), + user: (convertSeconds(process.cpuUsage().user /1000000)) + } + info.memoryUsage = {}; + for (const [key,value] of Object.entries(process.memoryUsage())){ + info.memoryUsage[key] = ([value]/1048576).toFixed(2) + 'Mb'; + } + } catch (ex) { } + } + else { + try { info.uptime = process.uptime(); } catch (ex) { } + try { info.cpuUsage = process.cpuUsage(); } catch (ex) { } + try { info.memoryUsage = process.memoryUsage(); } catch (ex) { } + } try { info.warnings = parent.parent.getServerWarnings(); } catch (ex) { console.log(ex); } try { info.allDevGroupManagers = parent.parent.config.settings.managealldevicegroups; } catch (ex) { } try { if (process.traceDeprecation == true) { info.traceDeprecation = true; } } catch (ex) { } @@ -7481,8 +7710,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use cmdData.result += 'Record: ' + parent.parent.recordpath + '\r\n'; cmdData.result += 'WebPublic: ' + parent.parent.webPublicPath + '\r\n'; cmdData.result += 'WebViews: ' + parent.parent.webViewsPath + '\r\n'; - if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebPublic: ' + parent.parent.webViewsOverridePath + '\r\n'; } - if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebViews: ' + parent.parent.webPublicOverridePath + '\r\n'; } + cmdData.result += 'WebEmails: ' + parent.parent.webEmailsPath + '\r\n'; + if (parent.parent.webPublicOverridePath) { cmdData.result += 'XWebPublic: ' + parent.parent.webPublicOverridePath + '\r\n'; } + if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebViews: ' + parent.parent.webViewsOverridePath + '\r\n'; } + if (parent.parent.webEmailsOverridePath) { cmdData.result += 'XWebEmails: ' + parent.parent.webEmailsOverridePath + '\r\n'; } + if (domain.webpublicpath) { cmdData.result += 'DomainWebPublic: ' + domain.webpublicpath + '\r\n'; } + if (domain.webviewspath) { cmdData.result += 'DomainWebViews: ' + domain.webviewspath + '\r\n'; } + if (domain.webemailspath) { cmdData.result += 'DomainWebEmails: ' + domain.webemailspath + '\r\n'; } } function serverUserCommandMigrationAgents(cmdData) { @@ -7537,10 +7771,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } function serverUserCommandAutoBackup(cmdData) { - var backupResult = parent.db.performBackup(function (msg) { + cmdData.result = parent.db.performBackup(function (msg) { try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { } }); - if (backupResult == 0) { cmdData.result = 'Starting auto-backup...'; } else { cmdData.result = 'Backup alreay in progress.'; } } function serverUserCommandBackupConfig(cmdData) { @@ -7645,6 +7878,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Session length if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { entry.length = docs[i].msgArgs[3]; } else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { entry.length = docs[i].msgArgs[0]; } + else if ((docs[i].msgid == 144) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[1] == 'number')) { entry.length = docs[i].msgArgs[1]; } if (command.groupBy == 1) { // Add entry to per user if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; } @@ -7711,6 +7945,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Session length if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { userEntry.length += docs[i].msgArgs[3]; } else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { userEntry.length += docs[i].msgArgs[0]; } + else if ((docs[i].msgid == 144) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[1] == 'number')) { userEntry.length += docs[i].msgArgs[1]; } // Set the user entry userEntries[docs[i].userid] = userEntry; @@ -7862,42 +8097,46 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.common.unEscapeAllLinksFieldName(docs); var results = [], resultPendingCount = 0; - for (i in docs) { - // Check device links, if a link points to an unknown user, remove it. - parent.cleanDevice(docs[i]); + if (docs.length == 0) { // no results return blank array + func(docs, type); + } else { + for (i in docs) { + // Check device links, if a link points to an unknown user, remove it. + parent.cleanDevice(docs[i]); - // Fetch the node from the database - resultPendingCount++; - const getNodeFunc = function (node, rights, visible) { - if ((node != null) && (visible == true)) { - const getNodeSysInfoFunc = function (err, docs) { - const getNodeNetInfoFunc = function (err, docs) { - var netinfo = null; - if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; } - resultPendingCount--; - getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo }); - if (resultPendingCount == 0) { func(getNodeFunc.results, type); } + // Fetch the node from the database + resultPendingCount++; + const getNodeFunc = function (node, rights, visible) { + if ((node != null) && (visible == true)) { + const getNodeSysInfoFunc = function (err, docs) { + const getNodeNetInfoFunc = function (err, docs) { + var netinfo = null; + if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; } + resultPendingCount--; + getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo }); + if (resultPendingCount == 0) { func(getNodeFunc.results, type); } + } + getNodeNetInfoFunc.results = getNodeSysInfoFunc.results; + getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid; + getNodeNetInfoFunc.node = getNodeSysInfoFunc.node; + if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; } + + // Query the database for network information + db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc); } - getNodeNetInfoFunc.results = getNodeSysInfoFunc.results; - getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid; - getNodeNetInfoFunc.node = getNodeSysInfoFunc.node; - if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; } + getNodeSysInfoFunc.results = getNodeFunc.results; + getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid; + getNodeSysInfoFunc.node = node; - // Query the database for network information - db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc); - } - getNodeSysInfoFunc.results = getNodeFunc.results; - getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid; - getNodeSysInfoFunc.node = node; - - // Query the database for system information - db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc); - } else { resultPendingCount--; } - if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); } + // Query the database for system information + db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc); + } else { resultPendingCount--; } + if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); } + } + getNodeFunc.results = results; + getNodeFunc.nodeid = docs[i]._id; + parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc); } - getNodeFunc.results = results; - getNodeFunc.nodeid = docs[i]._id; - parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc); } }); } else { @@ -8095,11 +8334,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)); var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))); var msg2fa = ((parent.parent.msgserver != null) && (parent.parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))); + var duo2fa = ((typeof domain.passwordrequirements != 'object') || (typeof domain.passwordrequirements.duo2factor == 'object')); var authFactorCount = 0; if (typeof user.otpsecret == 'string') { authFactorCount++; } // Authenticator time factor if (email2fa && (user.otpekey != null)) { authFactorCount++; } // EMail factor if (sms2fa && (user.phone != null)) { authFactorCount++; } // SMS factor if (msg2fa && (user.msghandle != null)) { authFactorCount++; } // Messaging factor + if (duo2fa && (user.otpduo != null)) { authFactorCount++; } // Duo authentication factor if (user.otphkeys != null) { authFactorCount += user.otphkeys.length; } // FIDO hardware factor if ((authFactorCount > 0) && (user.otpkeys != null)) { authFactorCount++; } // Backup keys return authFactorCount; diff --git a/monitoring.js b/monitoring.js new file mode 100644 index 00000000..ffb03da7 --- /dev/null +++ b/monitoring.js @@ -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; +} \ No newline at end of file diff --git a/multiserver.js b/multiserver.js index 623baefc..7d87d09d 100644 --- a/multiserver.js +++ b/multiserver.js @@ -586,6 +586,20 @@ module.exports.CreateMultiServer = function (parent, args) { } 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: { // Unknown peer server command console.log('Unknown action from peer server ' + peerServerId + ': ' + msg.action + '.'); @@ -634,7 +648,7 @@ module.exports.CreateMultiServer = function (parent, args) { peerTunnel.ws2.on('close', function (req) { peerTunnel.parent.parent.debug('peer', 'FTunnel disconnect ' + peerTunnel.serverid); peerTunnel.close(); }); // If a message is received from the peer, Peer ---> Browser (TODO: Pipe this?) - peerTunnel.ws2.on('message', function (msg) { try { peerTunnel.ws2._socket.pause(); peerTunnel.ws1.send(msg, function () { peerTunnel.ws2._socket.resume(); }); } catch (e) { } }); + peerTunnel.ws2.on('message', function (msg, isBinary) { try { peerTunnel.ws2._socket.pause(); peerTunnel.ws1.send((isBinary ? msg : msg.toString('binary')), function () { peerTunnel.ws2._socket.resume(); }); } catch (e) { } }); // Register the connection event peerTunnel.ws2.on('open', function () { diff --git a/package-lock.json b/package-lock.json index f7eeace6..caf31a34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,60 +1,81 @@ { "name": "meshcentral", - "version": "1.1.16", + "version": "1.1.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meshcentral", - "version": "1.1.16", + "version": "1.1.35", "license": "Apache-2.0", "dependencies": { - "@yetzt/nedb": "1.8.0", - "archiver": "5.3.2", - "body-parser": "1.20.2", + "@seald-io/nedb": "4.0.4", + "archiver": "7.0.1", + "body-parser": "1.20.3", "cbor": "5.2.0", - "compression": "1.7.4", - "cookie-session": "2.0.0", - "express": "4.18.2", - "express-handlebars": "5.3.5", - "express-ws": "4.0.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", "node-forge": "1.3.1", - "otplib": "10.2.3", - "ua-parser-js": "1.0.37", - "ws": "8.14.2", + "ua-parser-js": "1.0.39", + "ws": "8.18.0", "yauzl": "2.10.0" }, "bin": { "meshcentral": "bin/meshcentral" }, "engines": { - "node": ">=11.0.0" + "node": ">=16.0.0" } }, - "node_modules/@yetzt/binary-search-tree": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@yetzt/binary-search-tree/-/binary-search-tree-0.2.6.tgz", - "integrity": "sha512-e/8wt8AAumI8VK5sv09b3IgWuRoblXJ5z0SQYfrL2nap89oKihvVaP1zy3FzD5NaeRi1X0gdXZA9lB3QAZILBg==" + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } }, - "node_modules/@yetzt/nedb": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@yetzt/nedb/-/nedb-1.8.0.tgz", - "integrity": "sha512-1hUV/eIPSCRb4Vs9dgLekBCCawWNtf29immIF9kvzxnnnEoWgyFSDZgFvlFCiQ3Bzo8ifXn92HDS3l9fNvmtzA==", + "node_modules/@seald-io/binary-search-tree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz", + "integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==" + }, + "node_modules/@seald-io/nedb": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.0.4.tgz", + "integrity": "sha512-CUNcMio7QUHTA+sIJ/DC5JzVNNsHe743TPmC4H5Gij9zDLMbmrCT2li3eVB72/gF63BPS8pWEZrjlAMRKA8FDw==", + "license": "MIT", "dependencies": { - "@yetzt/binary-search-tree": "^0.2.6", - "async": "^3.2.0", + "@seald-io/binary-search-tree": "^1.0.3", "localforage": "^1.9.0", - "mkdirp": "^1.0.4", - "underscore": "^1.13.1" + "util": "^0.12.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" } }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -63,47 +84,92 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", "dependencies": { - "archiver-utils": "^2.1.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", "dependencies": { - "glob": "^7.1.4", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", + "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, - "node_modules/archiver-utils/node_modules/readable-stream": { + "node_modules/archiver-utils/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/archiver-utils/node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -114,33 +180,119 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/archiver-utils/node_modules/string_decoder": { + "node_modules/archiver-utils/node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", + "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", + "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "license": "Apache-2.0", + "optional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -159,30 +311,23 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -192,7 +337,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -203,18 +348,18 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -229,35 +374,72 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=8.0.0" } }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -267,6 +449,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "license": "MIT", "dependencies": { "bignumber.js": "^9.0.1", "nofilter": "^1.0.4" @@ -275,24 +458,29 @@ "node": ">=6.0.0" } }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 10" + "node": ">=7.0.0" } }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -301,39 +489,28 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -341,47 +518,31 @@ "node": ">= 0.6" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-session": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz", - "integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.0.tgz", + "integrity": "sha512-u73BDmR8QLGcs+Lprs0cfbcAPKl2HnPcjpwRXT41sEV4DRJ2+W0vJEEZkG31ofkx+HZflA70siRIjiTdIodmOQ==", + "license": "MIT", "dependencies": { - "cookies": "0.8.0", + "cookies": "0.9.1", "debug": "3.2.7", "on-headers": "~1.0.2", "safe-buffer": "5.2.1" @@ -394,6 +555,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -401,36 +563,20 @@ "node_modules/cookie-session/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/cookie-session/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", "dependencies": { "depd": "~2.0.0", "keygrip": "~1.1.0" @@ -442,12 +588,14 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" }, @@ -456,42 +604,79 @@ } }, "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", + "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -500,76 +685,148 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "license": "MIT", "dependencies": { - "once": "^1.4.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -578,27 +835,33 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-handlebars": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.5.tgz", - "integrity": "sha512-r9pzDc94ZNJ7FVvtsxLfPybmN0eFAUnR61oimNPRpD0D7nkLcezrkpZzoXS5TI75wYHRbflPLTU39B62pwB4DA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-7.1.3.tgz", + "integrity": "sha512-O0W4n14iQ8+iFIDdiMh9HRI2nbVQJ/h1qndlD1TXWxxcfbKjKoqJh+ti2tROkyx4C4VQrt0y3bANBQ5auQAiew==", + "license": "BSD-3-Clause", "dependencies": { - "glob": "^7.2.0", - "graceful-fs": "^4.2.8", - "handlebars": "^4.7.7" + "glob": "^10.4.2", + "graceful-fs": "^4.2.11", + "handlebars": "^4.7.8" }, "engines": { - "node": ">=v10.24.1" + "node": ">=v16" } }, "node_modules/express-ws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", - "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "license": "BSD-2-Clause", "dependencies": { - "ws": "^5.2.0" + "ws": "^7.4.6" }, "engines": { "node": ">=4.5.0" @@ -608,84 +871,49 @@ } }, "node_modules/express-ws/node_modules/ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "node": ">=8.3.0" }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "utf-8-validate": { + "optional": true } - ] + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -696,10 +924,48 @@ "node": ">= 0.8" } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -708,67 +974,71 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -777,12 +1047,14 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -800,20 +1072,22 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -821,10 +1095,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -833,9 +1111,10 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -847,6 +1126,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -862,6 +1142,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -886,31 +1167,26 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -918,17 +1194,211 @@ "node_modules/ipcheck": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ipcheck/-/ipcheck-0.1.0.tgz", - "integrity": "sha512-NwhrmROU0iXKa+U1quGuQ+ag+K/1Bb5V/yh5Q4SylSu/LGymPZcWB7p4u7JgJH0qOR6cTLDO5VZlRbhoeekNzQ==" + "integrity": "sha512-NwhrmROU0iXKa+U1quGuQ+ag+K/1Bb5V/yh5Q4SylSu/LGymPZcWB7p4u7JgJH0qOR6cTLDO5VZlRbhoeekNzQ==", + "license": "MIT" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jackspeak/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jackspeak/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jackspeak/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/jackspeak/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/jackspeak/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", "dependencies": { "tsscmp": "1.0.6" }, @@ -936,43 +1406,11 @@ "node": ">= 0.6" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -981,52 +1419,55 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1035,6 +1476,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -1043,9 +1485,10 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1054,6 +1497,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1061,45 +1505,59 @@ "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/multiparty": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz", "integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==", + "license": "MIT", "dependencies": { "http-errors": "~1.8.1", "safe-buffer": "5.2.1", @@ -1113,6 +1571,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1121,6 +1580,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", @@ -1132,7 +1592,234 @@ "node": ">= 0.6" } }, - "node_modules/multiparty/node_modules/safe-buffer": { + "node_modules/multiparty/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "license": "MIT" + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", @@ -1149,237 +1836,20 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/multiparty/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/otplib": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/otplib/-/otplib-10.2.3.tgz", - "integrity": "sha512-dwQTF4SkLFVZyV85JFrzCh+zSSlWHyKQtjbHrDmldxqBo6BMZ8uMfQ+kcVTf/VCkbUx1KARvn9cR/inYM2nHTw==", - "dependencies": { - "thirty-two": "1.0.2" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -1399,34 +1869,48 @@ "node": ">= 0.8.0" } }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1435,16 +1919,97 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1454,6 +2019,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1462,64 +2028,155 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/streamx": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/thirty-two": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", - "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=0.2.6" + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" } }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } @@ -1528,6 +2185,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", "engines": { "node": ">=0.6.x" } @@ -1536,6 +2194,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -1545,9 +2204,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", "funding": [ { "type": "opencollective", @@ -1562,14 +2221,19 @@ "url": "https://github.com/sponsors/faisalman" } ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -1582,6 +2246,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", "dependencies": { "random-bytes": "~1.0.0" }, @@ -1589,28 +2254,39 @@ "node": ">= 0.8" } }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -1619,24 +2295,74 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -1657,42 +2383,65 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", "engines": { - "node": ">= 10" + "node": "*" } }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", + "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } } } diff --git a/package.json b/package.json index 2ea9671e..78cd22bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "1.1.20", + "version": "1.1.42", "keywords": [ "Remote Device Management", "Remote Device Monitoring", @@ -37,25 +37,25 @@ "sample-config-advanced.json" ], "dependencies": { - "archiver": "5.3.2", - "body-parser": "1.20.2", + "@seald-io/nedb": "4.0.4", + "archiver": "7.0.1", + "body-parser": "1.20.3", "cbor": "5.2.0", - "compression": "1.7.4", - "cookie-session": "2.0.0", - "express": "4.18.2", - "express-handlebars": "5.3.5", - "express-ws": "4.0.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", - "@yetzt/nedb": "1.8.0", "node-forge": "1.3.1", - "ua-parser-js": "1.0.37", - "ws": "8.14.2", + "ua-parser-js": "1.0.39", + "ws": "8.18.0", "yauzl": "2.10.0" }, "engines": { - "node": ">=11.0.0" + "node": ">=16.0.0" }, "repository": { "type": "git", diff --git a/pluginHandler.js b/pluginHandler.js index 83138a4c..47c2a42d 100644 --- a/pluginHandler.js +++ b/pluginHandler.js @@ -139,7 +139,7 @@ module.exports.pluginHandler = function (parent) { try { obj.plugins[p][hookName](...args); } 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].content = obj.plugins[p].on_device_page(); } catch (e) { - console.log("Error ocurred while getting plugin views " + p + ':' + ' (' + e + ')'); + console.log("Error occurred while getting plugin views " + p + ':' + ' (' + e + ')'); } } } @@ -364,7 +364,7 @@ module.exports.pluginHandler = function (parent) { if (force_url != null) dl_url = force_url; var url = require('url'); var q = url.parse(dl_url, true); - var http = (q.protocol == "http") ? require('http') : require('https'); + var http = (q.protocol == "http:") ? require('http') : require('https'); var opts = { path: q.pathname, host: q.hostname, @@ -409,7 +409,13 @@ module.exports.pluginHandler = function (parent) { zipfile.openReadStream(entry, function (err, readStream) { if (err) throw err; readStream.on('end', function () { zipfile.readEntry(); }); - readStream.pipe(obj.fs.createWriteStream(filePath)); + if (process.platform == 'win32') { + readStream.pipe(obj.fs.createWriteStream(filePath)); + } else { + var fileMode = (entry.externalFileAttributes >> 16) & 0x0fff; + if( fileMode <= 0 ) fileMode = 0o644; + readStream.pipe(obj.fs.createWriteStream(filePath, { mode: fileMode })); + } }); } }); @@ -451,7 +457,7 @@ module.exports.pluginHandler = function (parent) { if (plugin.versionHistoryUrl == null) reject("No version history available for this plugin."); var url = require('url'); var q = url.parse(plugin.versionHistoryUrl, true); - var http = (q.protocol == 'http') ? require('http') : require('https'); + var http = (q.protocol == 'http:') ? require('http') : require('https'); var opts = { path: q.pathname, host: q.hostname, @@ -535,4 +541,4 @@ module.exports.pluginHandler = function (parent) { } } return obj; -}; \ No newline at end of file +}; diff --git a/public/images/duo-2fa-250-disable.png b/public/images/duo-2fa-250-disable.png new file mode 100644 index 00000000..01d91b18 Binary files /dev/null and b/public/images/duo-2fa-250-disable.png differ diff --git a/public/images/duo-2fa-250.png b/public/images/duo-2fa-250.png new file mode 100644 index 00000000..00941405 Binary files /dev/null and b/public/images/duo-2fa-250.png differ diff --git a/public/images/key16.png b/public/images/key16.png new file mode 100644 index 00000000..a184527f Binary files /dev/null and b/public/images/key16.png differ diff --git a/public/images/login/2fa-duo-48.png b/public/images/login/2fa-duo-48.png new file mode 100644 index 00000000..0c5d0afd Binary files /dev/null and b/public/images/login/2fa-duo-48.png differ diff --git a/public/images/login/2fa-duo-96.png b/public/images/login/2fa-duo-96.png new file mode 100644 index 00000000..5586963d Binary files /dev/null and b/public/images/login/2fa-duo-96.png differ diff --git a/public/images/MeshIcon100.png b/public/images/meshicon100.png similarity index 100% rename from public/images/MeshIcon100.png rename to public/images/meshicon100.png diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 145bfc45..00000000 --- a/public/manifest.json +++ /dev/null @@ -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" - } - ] -} \ No newline at end of file diff --git a/public/mstsc/client.js b/public/mstsc/client.js index 3294fe69..bc26d72b 100644 --- a/public/mstsc/client.js +++ b/public/mstsc/client.js @@ -180,6 +180,7 @@ self.prevClipboardText = null; self.clipboardReadTimer = setInterval(function(){ 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() .then(function(data){ if(data != self.prevClipboard){ diff --git a/public/novnc/app/locale/el.json b/public/novnc/app/locale/el.json index f801251c..4df3e03c 100644 --- a/public/novnc/app/locale/el.json +++ b/public/novnc/app/locale/el.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα", "Connecting...": "Συνδέεται...", "Disconnecting...": "Aποσυνδέεται...", "Reconnecting...": "Επανασυνδέεται...", @@ -7,19 +8,15 @@ "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", + "Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή", "Disconnected": "Αποσυνδέθηκε", "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", - "Password is required": "Απαιτείται ο κωδικός πρόσβασης", + "Credentials are required": "Απαιτούνται διαπιστευτήρια", "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:", "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου", + "Drag": "Σύρσιμο", "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου", - "viewport drag": "σύρσιμο θεατού πεδίου", - "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", - "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", - "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", - "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", - "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", "Keyboard": "Πληκτρολόγιο", "Show Keyboard": "Εμφάνιση Πληκτρολογίου", "Extra keys": "Επιπλέον πλήκτρα", @@ -28,6 +25,8 @@ "Toggle Ctrl": "Εναλλαγή Ctrl", "Alt": "Alt", "Toggle Alt": "Εναλλαγή Alt", + "Toggle Windows": "Εναλλαγή Παράθυρων", + "Windows": "Παράθυρα", "Send Tab": "Αποστολή Tab", "Tab": "Tab", "Esc": "Esc", @@ -41,8 +40,7 @@ "Reboot": "Επανεκκίνηση", "Reset": "Επαναφορά", "Clipboard": "Πρόχειρο", - "Clear": "Καθάρισμα", - "Fullscreen": "Πλήρης Οθόνη", + "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.", "Settings": "Ρυθμίσεις", "Shared Mode": "Κοινόχρηστη Λειτουργία", "View Only": "Μόνο Θέαση", @@ -52,6 +50,8 @@ "Local Scaling": "Τοπική Κλιμάκωση", "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", "Advanced": "Για προχωρημένους", + "Quality:": "Ποιότητα:", + "Compression level:": "Επίπεδο συμπίεσης:", "Repeater ID:": "Repeater ID:", "WebSocket": "WebSocket", "Encrypt": "Κρυπτογράφηση", @@ -60,10 +60,20 @@ "Path:": "Διαδρομή:", "Automatic Reconnect": "Αυτόματη επανασύνδεση", "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", + "Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας", "Logging:": "Καταγραφή:", + "Version:": "Έκδοση:", "Disconnect": "Αποσύνδεση", "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:": "Κωδικός Πρόσβασης:", - "Cancel": "Ακύρωση", - "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" + "Send Credentials": "Αποστολή Διαπιστευτηρίων", + "Cancel": "Ακύρωση" } \ No newline at end of file diff --git a/public/novnc/app/locale/fr.json b/public/novnc/app/locale/fr.json index 22531f73..c0eeec7d 100644 --- a/public/novnc/app/locale/fr.json +++ b/public/novnc/app/locale/fr.json @@ -1,5 +1,4 @@ { - "HTTPS is required for full functionality": "", "Connecting...": "En cours de connexion...", "Disconnecting...": "Déconnexion en cours...", "Reconnecting...": "Reconnexion en cours...", @@ -40,7 +39,8 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", - "Edit clipboard content in the textarea below.": "", + "Clear": "Effacer", + "Fullscreen": "Plein écran", "Settings": "Paramètres", "Shared Mode": "Mode partagé", "View Only": "Afficher uniquement", @@ -65,12 +65,6 @@ "Version:": "Version :", "Disconnect": "Dé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 :", "Password:": "Mot de passe :", "Send Credentials": "Envoyer les identifiants", diff --git a/public/novnc/app/locale/it.json b/public/novnc/app/locale/it.json index 6fd25702..18a7f744 100644 --- a/public/novnc/app/locale/it.json +++ b/public/novnc/app/locale/it.json @@ -14,8 +14,6 @@ "Credentials are required": "Le credenziali sono obbligatorie", "noVNC encountered an error:": "noVNC ha riscontrato un errore:", "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", - "Drag": "", - "Move/Drag Viewport": "", "Keyboard": "Tastiera", "Show Keyboard": "Mostra tastiera", "Extra keys": "Tasti Aggiuntivi", @@ -44,7 +42,6 @@ "Settings": "Impostazioni", "Shared Mode": "Modalità condivisa", "View Only": "Sola Visualizzazione", - "Clip to Window": "", "Scaling Mode:": "Modalità di ridimensionamento:", "None": "Nessuna", "Local Scaling": "Ridimensionamento Locale", @@ -61,7 +58,6 @@ "Automatic Reconnect": "Riconnessione Automatica", "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):", "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore", - "Logging:": "", "Version:": "Versione:", "Disconnect": "Disconnetti", "Connect": "Connetti", diff --git a/public/novnc/app/locale/ja.json b/public/novnc/app/locale/ja.json index 43fc5bf3..70fd7a5d 100644 --- a/public/novnc/app/locale/ja.json +++ b/public/novnc/app/locale/ja.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です", "Connecting...": "接続しています...", "Disconnecting...": "切断しています...", "Reconnecting...": "再接続しています...", @@ -21,10 +22,10 @@ "Extra keys": "追加キー", "Show Extra Keys": "追加キーを表示", "Ctrl": "Ctrl", - "Toggle Ctrl": "Ctrl キーを切り替え", + "Toggle Ctrl": "Ctrl キーをトグル", "Alt": "Alt", - "Toggle Alt": "Alt キーを切り替え", - "Toggle Windows": "Windows キーを切り替え", + "Toggle Alt": "Alt キーをトグル", + "Toggle Windows": "Windows キーをトグル", "Windows": "Windows", "Send Tab": "Tab キーを送信", "Tab": "Tab", @@ -39,11 +40,11 @@ "Reboot": "再起動", "Reset": "リセット", "Clipboard": "クリップボード", - "Clear": "クリア", - "Fullscreen": "全画面表示", + "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。", + "Full Screen": "全画面表示", "Settings": "設定", "Shared Mode": "共有モード", - "View Only": "表示のみ", + "View Only": "表示専用", "Clip to Window": "ウィンドウにクリップ", "Scaling Mode:": "スケーリングモード:", "None": "なし", @@ -60,11 +61,18 @@ "Path:": "パス:", "Automatic Reconnect": "自動再接続", "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", - "Show Dot when No Cursor": "カーソルがないときにドットを表示", + "Show Dot when No Cursor": "カーソルがないときにドットを表示する", "Logging:": "ロギング:", "Version:": "バージョン:", "Disconnect": "切断", "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:": "パスワード:", "Send Credentials": "資格情報を送信", diff --git a/public/novnc/app/locale/sv.json b/public/novnc/app/locale/sv.json index 077ef42c..80a400bf 100644 --- a/public/novnc/app/locale/sv.json +++ b/public/novnc/app/locale/sv.json @@ -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...", "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", "Internal error": "Internt fel", "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 (unencrypted) to ": "Ansluten (okrypterat) till ", "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", diff --git a/public/novnc/app/locale/zh_CN.json b/public/novnc/app/locale/zh_CN.json index f0aea9af..3679eadd 100644 --- a/public/novnc/app/locale/zh_CN.json +++ b/public/novnc/app/locale/zh_CN.json @@ -1,69 +1,69 @@ { "Connecting...": "连接中...", + "Connected (encrypted) to ": "已连接(已加密)到", + "Connected (unencrypted) to ": "已连接(未加密)到", "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": "已断开连接", - "New connection has been rejected with reason: ": "连接被拒绝,原因:", - "New connection has been rejected": "连接被拒绝", + "Must set host": "必须设置主机", + "Reconnecting...": "重新连接中...", "Password is required": "请提供密码", + "Disconnect timeout": "超时断开", "noVNC encountered an error:": "noVNC 遇到一个错误:", "Hide/Show the control bar": "显示/隐藏控制栏", - "Move/Drag Viewport": "拖放显示范围", - "viewport drag": "显示范围拖放", - "Active Mouse Button": "启动鼠标按鍵", - "No mousebutton": "禁用鼠标按鍵", - "Left mousebutton": "鼠标左鍵", - "Middle mousebutton": "鼠标中鍵", - "Right mousebutton": "鼠标右鍵", + "Move/Drag Viewport": "移动/拖动窗口", + "viewport drag": "窗口拖动", + "Active Mouse Button": "启动鼠标按键", + "No mousebutton": "禁用鼠标按键", + "Left mousebutton": "鼠标左键", + "Middle mousebutton": "鼠标中键", + "Right mousebutton": "鼠标右键", "Keyboard": "键盘", "Show Keyboard": "显示键盘", "Extra keys": "额外按键", "Show Extra Keys": "显示额外按键", "Ctrl": "Ctrl", "Toggle Ctrl": "切换 Ctrl", + "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", "Alt": "Alt", "Toggle Alt": "切换 Alt", "Send Tab": "发送 Tab 键", "Tab": "Tab", "Esc": "Esc", "Send Escape": "发送 Escape 键", - "Ctrl+Alt+Del": "Ctrl-Alt-Del", - "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键", - "Shutdown/Reboot": "关机/重新启动", - "Shutdown/Reboot...": "关机/重新启动...", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键", + "Shutdown/Reboot": "关机/重启", + "Shutdown/Reboot...": "关机/重启...", "Power": "电源", "Shutdown": "关机", - "Reboot": "重新启动", + "Reboot": "重启", "Reset": "重置", "Clipboard": "剪贴板", "Clear": "清除", "Fullscreen": "全屏", "Settings": "设置", + "Encrypt": "加密", "Shared Mode": "分享模式", "View Only": "仅查看", "Clip to Window": "限制/裁切窗口大小", "Scaling Mode:": "缩放模式:", "None": "无", "Local Scaling": "本地缩放", + "Local Downscaling": "降低本地尺寸", "Remote Resizing": "远程调整大小", "Advanced": "高级", + "Local Cursor": "本地光标", "Repeater ID:": "中继站 ID", "WebSocket": "WebSocket", - "Encrypt": "加密", "Host:": "主机:", "Port:": "端口:", "Path:": "路径:", "Automatic Reconnect": "自动重新连接", "Reconnect Delay (ms):": "重新连接间隔 (ms):", "Logging:": "日志级别:", - "Disconnect": "中断连接", + "Disconnect": "断开连接", "Connect": "连接", "Password:": "密码:", - "Cancel": "取消" + "Cancel": "取消", + "Canvas not supported.": "不支持 Canvas。" } \ No newline at end of file diff --git a/public/novnc/app/localization.js b/public/novnc/app/localization.js index 84341da6..7d7e6e6a 100644 --- a/public/novnc/app/localization.js +++ b/public/novnc/app/localization.js @@ -16,13 +16,19 @@ export class Localizer { this.language = 'en'; // Current dictionary of translations - this.dictionary = undefined; + this._dictionary = undefined; } // Configure suitable language based on user preferences - setup(supportedLanguages) { + async setup(supportedLanguages, baseURL) { 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+) * Fall back to navigator.language for other browsers @@ -40,12 +46,6 @@ export class Localizer { .replace("_", "-") .split("-"); - // Built-in default? - if ((userLang[0] === 'en') && - ((userLang[1] === undefined) || (userLang[1] === 'us'))) { - return; - } - // First pass: perfect match for (let j = 0; j < supportedLanguages.length; j++) { const supLang = supportedLanguages[j] @@ -64,7 +64,12 @@ export class Localizer { 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++) { const supLang = supportedLanguages[j] .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 get(id) { - if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { - return this.dictionary[id]; + if (typeof this._dictionary !== 'undefined' && + this._dictionary[id]) { + return this._dictionary[id]; } else { return id; } diff --git a/public/novnc/app/styles/base.css b/public/novnc/app/styles/base.css index 06e736a9..f83ad4b9 100644 --- a/public/novnc/app/styles/base.css +++ b/public/novnc/app/styles/base.css @@ -661,7 +661,7 @@ html { justify-content: center; align-content: center; - line-height: 25px; + line-height: 1.6; word-wrap: break-word; color: #fff; @@ -887,7 +887,7 @@ html { .noVNC_logo { color:yellow; font-family: 'Orbitron', 'OrbitronTTF', sans-serif; - line-height:90%; + line-height: 0.9; text-shadow: 0.1em 0.1em 0 black; } .noVNC_logo span{ diff --git a/public/novnc/app/styles/input.css b/public/novnc/app/styles/input.css index eaf083c7..dc345aab 100644 --- a/public/novnc/app/styles/input.css +++ b/public/novnc/app/styles/input.css @@ -86,6 +86,9 @@ option { * Checkboxes */ input[type=checkbox] { + display: inline-flex; + justify-content: center; + align-items: center; background-color: white; background-image: unset; border: 1px solid dimgrey; @@ -104,14 +107,11 @@ input[type=checkbox]:checked { input[type=checkbox]:checked::after { content: ""; display: block; /* width & height doesn't work on inline elements */ - position: relative; - top: 0; - left: 3px; width: 3px; height: 7px; border: 1px solid white; border-width: 0 2px 2px 0; - transform: rotate(40deg); + transform: rotate(40deg) translateY(-1px); } /* diff --git a/public/novnc/app/ui.js b/public/novnc/app/ui.js index 33a51a77..906f4c3f 100644 --- a/public/novnc/app/ui.js +++ b/public/novnc/app/ui.js @@ -18,6 +18,8 @@ import Keyboard from "../core/input/keyboard.js"; import RFB from "../core/rfb.js"; import * as WebUtil from "./webutil.js"; +const PAGE_TITLE = "noVNC"; + // String validation 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)) }; @@ -89,7 +91,7 @@ const UI = { // insecure context if (!window.isSecureContext) { // 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 @@ -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("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("serververification", UI.serverVerify); @@ -1166,6 +1175,7 @@ const UI = { UI.showStatus(_("Disconnected"), 'normal'); } + document.title = PAGE_TITLE; UI.openControlbar(); UI.openConnectPanel(); @@ -1739,9 +1749,9 @@ const UI = { }, updateDesktopName(e) { - // UI.desktopName = e.detail.name; + UI.desktopName = e.detail.name; // Display the desktop name in the document title - // document.title = e.detail.name + " - " + PAGE_TITLE; + document.title = e.detail.name + " - " + PAGE_TITLE; }, bell(e) { @@ -1778,20 +1788,8 @@ const UI = { // Set up translations const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; -l10n.setup(LINGUAS); -if (l10n.language === "en" || l10n.dictionary !== undefined) { - 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); -} +l10n.setup(LINGUAS, "app/locale/") + .catch(err => Log.Error("Failed to load translations: " + err)) + .then(UI.prime); export default UI; diff --git a/public/novnc/app/webutil.js b/public/novnc/app/webutil.js index 084c69f6..6011442c 100644 --- a/public/novnc/app/webutil.js +++ b/public/novnc/app/webutil.js @@ -6,16 +6,16 @@ * 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 export function initLogging(level) { "use strict"; if (typeof level !== "undefined") { - mainInitLogging(level); + Log.initLogging(level); } else { 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) // 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: -// https://www.example.com?nonsecretparam=example.com#password=secreatvalue +// https://www.example.com?nonsecretparam=example.com#password=secretvalue export function getQueryVar(name, defVal) { "use strict"; 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 (match) { @@ -146,7 +146,7 @@ export function writeSetting(name, value) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.set(settings); } 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)) { value = settings[name]; } else { - value = localStorage.getItem(name); + value = localStorageGet(name); settings[name] = value; } if (typeof value === "undefined") { @@ -181,6 +181,70 @@ export function eraseSetting(name) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); } 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; + } } } diff --git a/public/novnc/core/crypto/aes.js b/public/novnc/core/crypto/aes.js new file mode 100644 index 00000000..e6aaea7c --- /dev/null +++ b/public/novnc/core/crypto/aes.js @@ -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; + } +} diff --git a/public/novnc/core/crypto/bigint.js b/public/novnc/core/crypto/bigint.js new file mode 100644 index 00000000..d3443265 --- /dev/null +++ b/public/novnc/core/crypto/bigint.js @@ -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); +} diff --git a/public/novnc/core/crypto/crypto.js b/public/novnc/core/crypto/crypto.js new file mode 100644 index 00000000..cc17da22 --- /dev/null +++ b/public/novnc/core/crypto/crypto.js @@ -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; diff --git a/public/novnc/core/crypto/des.js b/public/novnc/core/crypto/des.js new file mode 100644 index 00000000..8dab31fb --- /dev/null +++ b/public/novnc/core/crypto/des.js @@ -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 , 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 . 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<>> 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; + } +} diff --git a/public/novnc/core/crypto/dh.js b/public/novnc/core/crypto/dh.js new file mode 100644 index 00000000..bd705d9b --- /dev/null +++ b/public/novnc/core/crypto/dh.js @@ -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); + } +} diff --git a/public/novnc/core/crypto/md5.js b/public/novnc/core/crypto/md5.js new file mode 100644 index 00000000..fcfefff0 --- /dev/null +++ b/public/novnc/core/crypto/md5.js @@ -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> 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; +} diff --git a/public/novnc/core/crypto/rsa.js b/public/novnc/core/crypto/rsa.js new file mode 100644 index 00000000..68e8e869 --- /dev/null +++ b/public/novnc/core/crypto/rsa.js @@ -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 }; + } +} diff --git a/public/novnc/core/decoders/hextile.js b/public/novnc/core/decoders/hextile.js index ac21eff0..cc33e0e1 100644 --- a/public/novnc/core/decoders/hextile.js +++ b/public/novnc/core/decoders/hextile.js @@ -31,10 +31,7 @@ export default class HextileDecoder { return false; } - let rQ = sock.rQ; - let rQi = sock.rQi; - - let subencoding = rQ[rQi]; // Peek + let subencoding = sock.rQpeek8(); if (subencoding > 30) { // Raw throw new Error("Illegal hextile subencoding (subencoding: " + subencoding + ")"); @@ -65,7 +62,7 @@ export default class HextileDecoder { return false; } - let subrects = rQ[rQi + bytes - 1]; // Peek + let subrects = sock.rQpeekBytes(bytes).at(-1); if (subencoding & 0x10) { // SubrectsColoured bytes += subrects * (4 + 2); } else { @@ -79,7 +76,7 @@ export default class HextileDecoder { } // We know the encoding and have a whole tile - rQi++; + sock.rQshift8(); if (subencoding === 0) { if (this._lastsubencoding & 0x01) { // Weird: ignore blanks are RAW @@ -89,42 +86,36 @@ export default class HextileDecoder { } } else if (subencoding & 0x01) { // Raw let pixels = tw * th; + let data = sock.rQshiftBytes(pixels * 4, false); // Max sure the image is fully opaque 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); - rQi += bytes - 1; + display.blitImage(tx, ty, tw, th, data, 0); } else { if (subencoding & 0x02) { // Background - this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._background = new Uint8Array(sock.rQshiftBytes(4)); } if (subencoding & 0x04) { // Foreground - this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._foreground = new Uint8Array(sock.rQshiftBytes(4)); } this._startTile(tx, ty, tw, th, this._background); if (subencoding & 0x08) { // AnySubrects - let subrects = rQ[rQi]; - rQi++; + let subrects = sock.rQshift8(); for (let s = 0; s < subrects; s++) { let color; if (subencoding & 0x10) { // SubrectsColoured - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + color = sock.rQshiftBytes(4); } else { color = this._foreground; } - const xy = rQ[rQi]; - rQi++; + const xy = sock.rQshift8(); const sx = (xy >> 4); const sy = (xy & 0x0f); - const wh = rQ[rQi]; - rQi++; + const wh = sock.rQshift8(); const sw = (wh >> 4) + 1; const sh = (wh & 0x0f) + 1; @@ -133,7 +124,6 @@ export default class HextileDecoder { } this._finishTile(display); } - sock.rQi = rQi; this._lastsubencoding = subencoding; this._tiles--; } diff --git a/public/novnc/core/decoders/jpeg.js b/public/novnc/core/decoders/jpeg.js index e1f2bdf8..feb2aeb6 100644 --- a/public/novnc/core/decoders/jpeg.js +++ b/public/novnc/core/decoders/jpeg.js @@ -11,131 +11,136 @@ export default class JPEGDecoder { constructor() { // RealVNC will reuse the quantization tables // and Huffman tables, so we need to cache them. - this._quantTables = []; - this._huffmanTables = []; this._cachedQuantTables = []; this._cachedHuffmanTables = []; - this._jpegLength = 0; this._segments = []; } decodeRect(x, y, width, height, sock, display, depth) { // 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) { - let j = i; - if (j + 2 > bufferLength) { + let segment = this._readSegment(sock); + if (segment === null) { 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); - 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; } } diff --git a/public/novnc/core/decoders/raw.js b/public/novnc/core/decoders/raw.js index d08f7ba9..3c166142 100644 --- a/public/novnc/core/decoders/raw.js +++ b/public/novnc/core/decoders/raw.js @@ -24,41 +24,34 @@ export default class RawDecoder { const pixelSize = depth == 8 ? 1 : 4; const bytesPerLine = width * pixelSize; - 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; + while (this._lines > 0) { + if (sock.rQwait("RAW", bytesPerLine)) { + return false; } - data = newdata; - index = 0; - } - // Max sure the image is fully opaque - for (let i = 0; i < pixels; i++) { - data[index + i * 4 + 3] = 255; - } + const curY = y + (height - this._lines); - display.blitImage(x, curY, width, currHeight, data, index); - sock.rQskipBytes(currHeight * bytesPerLine); - this._lines -= currHeight; - if (this._lines > 0) { - return false; + let data = sock.rQshiftBytes(bytesPerLine, false); + + // Convert data if needed + if (depth == 8) { + 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; diff --git a/public/novnc/core/decoders/tight.js b/public/novnc/core/decoders/tight.js index 7952707c..8bc977a7 100644 --- a/public/novnc/core/decoders/tight.js +++ b/public/novnc/core/decoders/tight.js @@ -76,12 +76,8 @@ export default class TightDecoder { return false; } - const rQi = sock.rQi; - const rQ = sock.rQ; - - display.fillRect(x, y, width, height, - [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); - sock.rQskipBytes(3); + let pixel = sock.rQshiftBytes(3); + display.fillRect(x, y, width, height, pixel, false); return true; } @@ -289,7 +285,73 @@ export default class TightDecoder { } _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) { @@ -316,7 +378,7 @@ export default class TightDecoder { return null; } - let data = sock.rQshiftBytes(this._len); + let data = sock.rQshiftBytes(this._len, false); this._len = 0; return data; diff --git a/public/novnc/core/decoders/zrle.js b/public/novnc/core/decoders/zrle.js index 97fbd58e..49128e79 100644 --- a/public/novnc/core/decoders/zrle.js +++ b/public/novnc/core/decoders/zrle.js @@ -32,7 +32,7 @@ export default class ZRLEDecoder { return false; } - const data = sock.rQshiftBytes(this._length); + const data = sock.rQshiftBytes(this._length, false); this._inflator.setInput(data); diff --git a/public/novnc/core/deflator.js b/public/novnc/core/deflator.js index fe2a8f70..22f6770b 100644 --- a/public/novnc/core/deflator.js +++ b/public/novnc/core/deflator.js @@ -7,7 +7,7 @@ */ 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"; export default class Deflator { @@ -15,9 +15,8 @@ export default class Deflator { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.outputBuffer = new Uint8Array(this.chunkSize); - this.windowBits = 5; - deflateInit(this.strm, this.windowBits); + deflateInit(this.strm, Z_DEFAULT_COMPRESSION); } deflate(inData) { diff --git a/public/novnc/core/display.js b/public/novnc/core/display.js index bf8d5fab..fcd62699 100644 --- a/public/novnc/core/display.js +++ b/public/novnc/core/display.js @@ -15,7 +15,7 @@ export default class Display { this._drawCtx = null; this._renderQ = []; // queue drawing actions for in-oder rendering - this._flushing = false; + this._flushPromise = null; // the full frame buffer (logical canvas) size this._fbWidth = 0; @@ -61,10 +61,6 @@ export default class Display { this._scale = 1.0; this._clipViewport = false; - - // ===== EVENT HANDLERS ===== - - this.onflush = () => {}; // A flush request has finished } // ===== PROPERTIES ===== @@ -306,9 +302,14 @@ export default class Display { flush() { if (this._renderQ.length === 0) { - this.onflush(); + return Promise.resolve(); } 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) { - this._flushing = false; - this.onflush(); + if (this._renderQ.length === 0 && + this._flushPromise !== null) { + this._flushResolve(); + this._flushPromise = null; + this._flushResolve = null; } } } diff --git a/public/novnc/core/encodings.js b/public/novnc/core/encodings.js index 2041b6e0..1a79989d 100644 --- a/public/novnc/core/encodings.js +++ b/public/novnc/core/encodings.js @@ -22,6 +22,7 @@ export const encodings = { pseudoEncodingLastRect: -224, pseudoEncodingCursor: -239, pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingQEMULedEvent: -261, pseudoEncodingDesktopName: -307, pseudoEncodingExtendedDesktopSize: -308, pseudoEncodingXvp: -309, diff --git a/public/novnc/core/inflator.js b/public/novnc/core/inflator.js index 4b337607..f851f2a7 100644 --- a/public/novnc/core/inflator.js +++ b/public/novnc/core/inflator.js @@ -14,9 +14,8 @@ export default class Inflate { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.strm.output = new Uint8Array(this.chunkSize); - this.windowBits = 5; - inflateInit(this.strm, this.windowBits); + inflateInit(this.strm); } setInput(data) { diff --git a/public/novnc/core/input/keyboard.js b/public/novnc/core/input/keyboard.js index ddb5ce09..68da2312 100644 --- a/public/novnc/core/input/keyboard.js +++ b/public/novnc/core/input/keyboard.js @@ -36,7 +36,7 @@ export default class Keyboard { // ===== PRIVATE METHODS ===== - _sendKeyEvent(keysym, code, down) { + _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) { if (down) { this._keyDownList[code] = keysym; } else { @@ -48,8 +48,9 @@ export default class Keyboard { } Log.Debug("onkeyevent " + (down ? "down" : "up") + - ", keysym: " + keysym, ", code: " + code); - this.onkeyevent(keysym, code, down); + ", keysym: " + keysym, ", code: " + code + + ", numlock: " + numlock + ", capslock: " + capslock); + this.onkeyevent(keysym, code, down, numlock, capslock); } _getKeyCode(e) { @@ -86,6 +87,14 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(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 // fake Ctrl+Alt. However the remote end might not be Windows, @@ -107,7 +116,7 @@ export default class Keyboard { // key to "AltGraph". keysym = KeyTable.XK_ISO_Level3_Shift; } 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 // sufficient to just send press and release right // after each other - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); } stopEvent(e); @@ -157,8 +166,8 @@ export default class Keyboard { // while meta is held down if ((browser.isMac() || browser.isIOS()) && (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -168,8 +177,8 @@ export default class Keyboard { // which toggles on each press, but not on release. So pretend // it was a quick press and release of the button. if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock); stopEvent(e); return; } @@ -182,8 +191,8 @@ export default class Keyboard { KeyTable.XK_Hiragana, KeyTable.XK_Romaji ]; if (browser.isWindows() && jpBadKeys.includes(keysym)) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -199,7 +208,7 @@ export default class Keyboard { return; } - this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, true, numlock, capslock); } _handleKeyUp(e) { diff --git a/public/novnc/core/input/util.js b/public/novnc/core/input/util.js index 58f84e55..36b69817 100644 --- a/public/novnc/core/input/util.js +++ b/public/novnc/core/input/util.js @@ -67,7 +67,7 @@ export function getKeycode(evt) { // Get 'KeyboardEvent.key', handling legacy browsers export function getKey(evt) { // 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 switch (evt.key) { case 'OS': return 'Meta'; diff --git a/public/novnc/core/ra2.js b/public/novnc/core/ra2.js index 81a8a895..d330b848 100644 --- a/public/novnc/core/ra2.js +++ b/public/novnc/core/ra2.js @@ -1,146 +1,25 @@ -import Base64 from './base64.js'; import { encodeUTF8 } from './util/strings.js'; import EventTargetMixin from './util/eventtarget.js'; +import legacyCrypto from './crypto/crypto.js'; -export class AESEAXCipher { +class RA2Cipher { 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]); - } - - 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._cipher = null; this._counter = new Uint8Array(16); } async setKey(key) { - await this._cipher.setKey(key); + this._cipher = await legacyCrypto.importKey( + "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]); } async makeMessage(message) { 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++); const res = new Uint8Array(message.length + 2 + 16); res.set(ad); @@ -148,164 +27,18 @@ export class RA2Cipher { return res; } - async receiveMessage(length, encrypted, mac) { + async receiveMessage(length, encrypted) { 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++); 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 { constructor(sock, getCredentials) { super(); @@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { this._hasStarted = true; // 1: Receive server public key await this._waitSockAsync(4); - const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLengthBuffer = this._sock.rQpeekBytes(4); const serverKeyLength = this._sock.rQshift32(); if (serverKeyLength < 1024) { 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); const serverN = this._sock.rQshiftBytes(serverKeyBytes); const serverE = this._sock.rQshiftBytes(serverKeyBytes); - const serverRSACipher = new RSACipher(serverKeyLength); - serverRSACipher.setPublicKey(serverN, serverE); + const serverRSACipher = await legacyCrypto.importKey( + "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]); const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); serverPublickey.set(serverKeyLengthBuffer); serverPublickey.set(serverN, 4); serverPublickey.set(serverE, 4 + serverKeyBytes); // verify server public key + let approveKey = this._waitApproveKeyAsync(); this.dispatchEvent(new CustomEvent("serververification", { detail: { type: "RSA", publickey: serverPublickey } })); - await this._waitApproveKeyAsync(); + await approveKey; // 2: Send client public key const clientKeyLength = 2048; const clientKeyBytes = Math.ceil(clientKeyLength / 8); - const clientRSACipher = new RSACipher(clientKeyLength); - await clientRSACipher.generateKey(); - const clientN = clientRSACipher.n; - const clientE = clientRSACipher.e; + const clientRSACipher = (await legacyCrypto.generateKey({ + name: "RSA-PKCS1-v1_5", + modulusLength: clientKeyLength, + 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); clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; @@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { clientPublicKey[3] = clientKeyLength & 0xff; clientPublicKey.set(clientN, 4); clientPublicKey.set(clientE, 4 + clientKeyBytes); - this._sock.send(clientPublicKey); + this._sock.sQpushBytes(clientPublicKey); + this._sock.flush(); // 3: Send client random const clientRandom = new Uint8Array(16); 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); clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; clientRandomMessage[1] = serverKeyBytes & 0xff; clientRandomMessage.set(clientEncryptedRandom, 2); - this._sock.send(clientRandomMessage); + this._sock.sQpushBytes(clientRandomMessage); + this._sock.flush(); // 4: Receive server random await this._waitSockAsync(2); @@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { throw new Error("RA2: wrong encrypted message length"); } 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) { 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); serverHash = new Uint8Array(serverHash); 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); if (this._sock.rQshift16() !== 20) { throw new Error("RA2: wrong server hash"); } const serverHashReceived = await serverCipher.receiveMessage( - 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); + 20, this._sock.rQshiftBytes(20 + 16)); if (serverHashReceived === null) { 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"); } let subtype = (await serverCipher.receiveMessage( - 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); + 1, this._sock.rQshiftBytes(1 + 16))); if (subtype === null) { throw new Error("RA2: failed to authenticate the message"); } subtype = subtype[0]; + let waitCredentials = this._waitCredentialsAsync(subtype); if (subtype === 1) { if (this._getCredentials().username === undefined || this._getCredentials().password === undefined) { @@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { } else { throw new Error("RA2: wrong subtype"); } - await this._waitCredentialsAsync(subtype); + await waitCredentials; let username; if (subtype === 1) { 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++) { 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() { @@ -564,4 +309,4 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { set hasStarted(s) { this._hasStarted = s; } -} \ No newline at end of file +} diff --git a/public/novnc/core/rfb.js b/public/novnc/core/rfb.js index 6afd7c65..f2deb0e7 100644 --- a/public/novnc/core/rfb.js +++ b/public/novnc/core/rfb.js @@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js"; import GestureHandler from "./input/gesturehandler.js"; import Cursor from "./util/cursor.js"; import Websock from "./websock.js"; -import DES from "./des.js"; import KeyTable from "./input/keysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.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 CopyRectDecoder from "./decoders/copyrect.js"; @@ -258,10 +257,11 @@ export default class RFB extends EventTargetMixin { Log.Error("Display exception: " + exc); throw exc; } - this._display.onflush = this._onFlush.bind(this); this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + this._remoteCapsLock = null; // Null indicates unknown or irrelevant + this._remoteNumLock = null; this._gestures = new GestureHandler(); @@ -960,7 +960,7 @@ export default class RFB extends EventTargetMixin { } _handleMessage() { - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { Log.Warn("handleMessage called on an empty receive queue"); return; } @@ -977,7 +977,7 @@ export default class RFB extends EventTargetMixin { if (!this._normalMsg()) { break; } - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { 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); } @@ -1383,7 +1411,8 @@ export default class RFB extends EventTargetMixin { while (repeaterID.length < 250) { repeaterID += "\0"; } - this._sock.sendString(repeaterID); + this._sock.sQpushString(repeaterID); + this._sock.flush(); return true; } @@ -1393,7 +1422,8 @@ export default class RFB extends EventTargetMixin { const cversion = "00" + parseInt(this._rfbVersion, 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); this._rfbInitState = 'Security'; @@ -1445,7 +1475,8 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + types + ")"); } - this._sock.send([this._rfbAuthScheme]); + this._sock.sQpush8(this._rfbAuthScheme); + this._sock.flush(); } else { // Server decides if (this._sock.rQwait("security scheme", 4)) { return false; } @@ -1507,12 +1538,15 @@ export default class RFB extends EventTargetMixin { return false; } - const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) + - String.fromCharCode(this._rfbCredentials.target.length) + - this._rfbCredentials.username + - this._rfbCredentials.target; - this._sock.sendString(xvpAuthStr); + this._sock.sQpush8(this._rfbCredentials.username.length); + this._sock.sQpush8(this._rfbCredentials.target.length); + this._sock.sQpushString(this._rfbCredentials.username); + this._sock.sQpushString(this._rfbCredentials.target); + + this._sock.flush(); + this._rfbAuthScheme = securityTypeVNCAuth; + return this._negotiateAuthentication(); } @@ -1530,7 +1564,9 @@ export default class RFB extends EventTargetMixin { 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; } @@ -1589,12 +1625,10 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + subtypes + ")"); } - this._sock.send([this._rfbAuthScheme >> 24, - this._rfbAuthScheme >> 16, - this._rfbAuthScheme >> 8, - this._rfbAuthScheme]); + this._sock.sQpush32(this._rfbAuthScheme); + this._sock.flush(); - this._rfbVeNCryptState == 4; + this._rfbVeNCryptState = 4; return true; } } @@ -1611,20 +1645,11 @@ export default class RFB extends EventTargetMixin { const user = encodeUTF8(this._rfbCredentials.username); const pass = encodeUTF8(this._rfbCredentials.password); - this._sock.send([ - (user.length >> 24) & 0xFF, - (user.length >> 16) & 0xFF, - (user.length >> 8) & 0xFF, - user.length & 0xFF - ]); - 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._sock.sQpush32(user.length); + this._sock.sQpush32(pass.length); + this._sock.sQpushString(user); + this._sock.sQpushString(pass); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; @@ -1643,7 +1668,8 @@ export default class RFB extends EventTargetMixin { // TODO(directxman12): make genDES not require an Array const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); const response = RFB.genDES(this._rfbCredentials.password, challenge); - this._sock.send(response); + this._sock.sQpushBytes(response); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -1661,8 +1687,9 @@ export default class RFB extends EventTargetMixin { if (this._rfbCredentials.ardPublicKey != undefined && this._rfbCredentials.ardCredentials != undefined) { // if the async web crypto is done return the results - this._sock.send(this._rfbCredentials.ardCredentials); - this._sock.send(this._rfbCredentials.ardPublicKey); + this._sock.sQpushBytes(this._rfbCredentials.ardCredentials); + this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey); + this._sock.flush(); this._rfbCredentials.ardCredentials = null; this._rfbCredentials.ardPublicKey = null; this._rfbInitState = "SecurityResult"; @@ -1681,77 +1708,35 @@ export default class RFB extends EventTargetMixin { let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key - let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); - let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); - - this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding); + let clientKey = legacyCrypto.generateKey( + { name: "DH", g: generator, p: prime }, false, ["deriveBits"]); + this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey); 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(''); - let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); - let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63); + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - let b = BigInt(baseHex); - let e = BigInt(exponentHex); - let m = BigInt(modulusHex); - let r = 1n; - b = b % m; - while (e > 0) { - if (e % 2n === 1n) { - r = (r * b) % m; - } - e = e / 2n; - b = (b * b) % m; + const credentials = window.crypto.getRandomValues(new Uint8Array(128)); + for (let i = 0; i < username.length; i++) { + credentials[i] = username.charCodeAt(i); } - let hexResult = r.toString(16); - - while (hexResult.length/2 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 { - this.dispatchEvent(new CustomEvent('securityresult')); + }) + .then(() => { this._rfbInitState = "SecurityResult"; return true; }).finally(() => { @@ -1934,15 +1923,15 @@ export default class RFB extends EventTargetMixin { const g = this._sock.rQshiftBytes(8); const p = this._sock.rQshiftBytes(8); const A = this._sock.rQshiftBytes(8); - const b = window.crypto.getRandomValues(new Uint8Array(8)); - const B = new Uint8Array(this._modPow(g, b, p)); - const secret = new Uint8Array(this._modPow(A, b, p)); + const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]); + const B = legacyCrypto.exportKey("raw", dhKey.publicKey); + 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 password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - const usernameBytes = new Uint8Array(256); - const passwordBytes = new Uint8Array(64); + let usernameBytes = new Uint8Array(256); + let passwordBytes = new Uint8Array(64); window.crypto.getRandomValues(usernameBytes); window.crypto.getRandomValues(passwordBytes); for (let i = 0; i < username.length; i++) { @@ -1953,25 +1942,12 @@ export default class RFB extends EventTargetMixin { passwordBytes[i] = password.charCodeAt(i); } passwordBytes[password.length] = 0; - let x = new Uint8Array(secret); - for (let i = 0; i < 32; i++) { - for (let j = 0; j < 8; j++) { - x[j] ^= usernameBytes[i * 8 + j]; - } - x = des.enc8(x); - 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); + usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes); + passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes); + this._sock.sQpushBytes(B); + this._sock.sQpushBytes(usernameBytes); + this._sock.sQpushBytes(passwordBytes); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -1979,7 +1955,11 @@ export default class RFB extends EventTargetMixin { _negotiateAuthentication() { switch (this._rfbAuthScheme) { case securityTypeNone: - this._rfbInitState = 'SecurityResult'; + if (this._rfbVersion >= 3.8) { + this._rfbInitState = 'SecurityResult'; + } else { + this._rfbInitState = 'ClientInitialisation'; + } return true; case securityTypeXVP: @@ -2016,13 +1996,6 @@ export default class RFB extends EventTargetMixin { } _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; } const status = this._sock.rQshift32(); @@ -2158,6 +2131,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingDesktopSize); encs.push(encodings.pseudoEncodingLastRect); encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingQEMULedEvent); encs.push(encodings.pseudoEncodingExtendedDesktopSize); encs.push(encodings.pseudoEncodingXvp); encs.push(encodings.pseudoEncodingFence); @@ -2199,7 +2173,8 @@ export default class RFB extends EventTargetMixin { return this._handleSecurityReason(); case 'ClientInitialisation': - this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation + this._sock.flush(); this._rfbInitState = 'ServerInitialisation'; return true; @@ -2381,7 +2356,7 @@ export default class RFB extends EventTargetMixin { textData = textData.slice(0, -1); } - textData = textData.replace("\r\n", "\n"); + textData = textData.replaceAll("\r\n", "\n"); this.dispatchEvent(new CustomEvent( "clipboard", @@ -2512,19 +2487,11 @@ export default class RFB extends EventTargetMixin { default: 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; } } - _onFlush() { - this._flushing = false; - // Resume processing - if (this._sock.rQlen > 0) { - this._handleMessage(); - } - } - _framebufferUpdate() { if (this._FBU.rects === 0) { 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 if (this._display.pending()) { 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; } } @@ -2545,13 +2519,13 @@ export default class RFB extends EventTargetMixin { if (this._sock.rQwait("rect header", 12)) { return false; } /* New FramebufferUpdate */ - const hdr = this._sock.rQshiftBytes(12); - this._FBU.x = (hdr[0] << 8) + hdr[1]; - this._FBU.y = (hdr[2] << 8) + hdr[3]; - this._FBU.width = (hdr[4] << 8) + hdr[5]; - this._FBU.height = (hdr[6] << 8) + hdr[7]; - this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); + this._FBU.x = this._sock.rQshift16(); + this._FBU.y = this._sock.rQshift16(); + this._FBU.width = this._sock.rQshift16(); + this._FBU.height = this._sock.rQshift16(); + this._FBU.encoding = this._sock.rQshift32(); + /* Encodings are signed */ + this._FBU.encoding >>= 0; } if (!this._handleRect()) { @@ -2593,6 +2567,9 @@ export default class RFB extends EventTargetMixin { case encodings.pseudoEncodingExtendedDesktopSize: return this._handleExtendedDesktopSize(); + case encodings.pseudoEncodingQEMULedEvent: + return this._handleLedEvent(); + default: return this._handleDataRect(); } @@ -2770,6 +2747,21 @@ export default class RFB extends EventTargetMixin { 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() { if (this._sock.rQwait("ExtendedDesktopSize", 4)) { return false; @@ -2785,26 +2777,18 @@ export default class RFB extends EventTargetMixin { const firstUpdate = !this._supportsSetDesktopSize; 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(3); // padding for (let i = 0; i < numberOfScreens; i += 1) { // Save the id and flags of the first screen if (i === 0) { - this._screenID = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height - this._screenFlags = this._sock.rQshiftBytes(4); // flags + this._screenID = this._sock.rQshift32(); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screenFlags = this._sock.rQshift32(); // flags } else { this._sock.rQskipBytes(16); } @@ -2842,6 +2826,14 @@ export default class RFB extends EventTargetMixin { 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; } @@ -2937,28 +2929,22 @@ export default class RFB extends EventTargetMixin { static genDES(password, challenge) { 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 RFB.messages = { keyEvent(sock, keysym, down) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(4); // msg-type + sock.sQpush8(down); - buff[offset] = 4; // msg-type - buff[offset + 1] = down; + sock.sQpush16(0); - buff[offset + 2] = 0; - buff[offset + 3] = 0; + sock.sQpush32(keysym); - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - sock._sQlen += 8; sock.flush(); }, @@ -2972,46 +2958,28 @@ RFB.messages = { return xtScanCode; } - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(255); // msg-type + sock.sQpush8(0); // sub msg-type - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type + sock.sQpush16(down); - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; + sock.sQpush32(keysym); const RFBkeycode = getRFBkeycode(keycode); - buff[offset + 8] = (RFBkeycode >> 24); - buff[offset + 9] = (RFBkeycode >> 16); - buff[offset + 10] = (RFBkeycode >> 8); - buff[offset + 11] = RFBkeycode; + sock.sQpush32(RFBkeycode); - sock._sQlen += 12; sock.flush(); }, pointerEvent(sock, x, y, mask) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(5); // msg-type - 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(); }, @@ -3111,14 +3079,11 @@ RFB.messages = { }, clientCutText(sock, data, extended = false) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(6); // msg-type - buff[offset] = 6; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding let length; if (extended) { @@ -3127,121 +3092,63 @@ RFB.messages = { length = data.length; } - buff[offset + 4] = length >> 24; - buff[offset + 5] = length >> 16; - buff[offset + 6] = length >> 8; - 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; - } - + sock.sQpush32(length); + sock.sQpushBytes(data); + sock.flush(); }, setDesktopSize(sock, width, height, id, flags) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(251); // msg-type - buff[offset] = 251; // msg-type - 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; + sock.sQpush8(0); // padding - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding + sock.sQpush16(width); + sock.sQpush16(height); + + sock.sQpush8(1); // number-of-screens + + sock.sQpush8(0); // padding // screen array - buff[offset + 8] = id >> 24; // id - buff[offset + 9] = id >> 16; - buff[offset + 10] = id >> 8; - buff[offset + 11] = id; - buff[offset + 12] = 0; // x-position - buff[offset + 13] = 0; - 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.sQpush32(id); + sock.sQpush16(0); // x-position + sock.sQpush16(0); // y-position + sock.sQpush16(width); + sock.sQpush16(height); + sock.sQpush32(flags); - sock._sQlen += 24; sock.flush(); }, clientFence(sock, flags, payload) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(248); // msg-type - buff[offset] = 248; // msg-type + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush32(flags); - buff[offset + 4] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; + sock.sQpush8(payload.length); + sock.sQpushString(payload); - 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(); }, enableContinuousUpdates(sock, enable, x, y, width, height) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(150); // msg-type - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag + sock.sQpush8(enable); - buff[offset + 2] = x >> 8; // x - buff[offset + 3] = x; - buff[offset + 4] = y >> 8; // y - buff[offset + 5] = y; - buff[offset + 6] = width >> 8; // width - buff[offset + 7] = width; - buff[offset + 8] = height >> 8; // height - buff[offset + 9] = height; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(width); + sock.sQpush16(height); - sock._sQlen += 10; sock.flush(); }, pixelFormat(sock, depth, trueColor) { - const buff = sock._sQ; - const offset = sock._sQlen; - let bpp; if (depth > 16) { @@ -3254,100 +3161,69 @@ RFB.messages = { const bits = Math.floor(depth/3); - buff[offset] = 0; // msg-type + sock.sQpush8(0); // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 4] = bpp; // bits-per-pixel - buff[offset + 5] = depth; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = trueColor ? 1 : 0; // true-color + sock.sQpush8(bpp); + sock.sQpush8(depth); + sock.sQpush8(0); // little-endian + sock.sQpush8(trueColor ? 1 : 0); - buff[offset + 8] = 0; // red-max - buff[offset + 9] = (1 << bits) - 1; // red-max + sock.sQpush16((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 - buff[offset + 11] = (1 << bits) - 1; // green-max + sock.sQpush8(bits * 0); // red-shift + sock.sQpush8(bits * 1); // green-shift + sock.sQpush8(bits * 2); // blue-shift - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = (1 << bits) - 1; // blue-max + sock.sQpush8(0); // padding + 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(); }, clientEncodings(sock, encodings) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(2); // msg-type - buff[offset] = 2; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = encodings.length >> 8; - buff[offset + 3] = encodings.length; - - let j = offset + 4; + sock.sQpush16(encodings.length); for (let i = 0; i < encodings.length; i++) { - const enc = encodings[i]; - buff[j] = enc >> 24; - buff[j + 1] = enc >> 16; - buff[j + 2] = enc >> 8; - buff[j + 3] = enc; - - j += 4; + sock.sQpush32(encodings[i]); } - sock._sQlen += j - offset; sock.flush(); }, fbUpdateRequest(sock, incremental, x, y, w, h) { - const buff = sock._sQ; - const offset = sock._sQlen; - if (typeof(x) === "undefined") { x = 0; } if (typeof(y) === "undefined") { y = 0; } - buff[offset] = 3; // msg-type - buff[offset + 1] = incremental ? 1 : 0; + sock.sQpush8(3); // msg-type - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; + sock.sQpush8(incremental ? 1 : 0); - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; + sock.sQpush16(x); + 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(); }, xvpOp(sock, ver, op) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(250); // msg-type - buff[offset] = 250; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = ver; - buff[offset + 3] = op; + sock.sQpush8(ver); + sock.sQpush8(op); - sock._sQlen += 4; sock.flush(); } }; diff --git a/public/novnc/core/util/cursor.js b/public/novnc/core/util/cursor.js index 3000cf0e..20e75f1b 100644 --- a/public/novnc/core/util/cursor.js +++ b/public/novnc/core/util/cursor.js @@ -69,7 +69,9 @@ export default class Cursor { this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, 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; diff --git a/public/novnc/core/websock.js b/public/novnc/core/websock.js index 37b33fcc..61a3091a 100644 --- a/public/novnc/core/websock.js +++ b/public/novnc/core/websock.js @@ -94,27 +94,7 @@ export default class Websock { 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 - get rQlen() { - return this._rQlen - this._rQi; - } - rQpeek8() { return this._rQ[this._rQi]; } @@ -141,42 +121,47 @@ export default class Websock { for (let byte = bytes - 1; byte >= 0; byte--) { res += this._rQ[this._rQi++] << (byte * 8); } - return res; + return res >>> 0; } rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } let str = ""; // Handle large arrays in steps to avoid long strings on the stack 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); } return str; } - rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + rQshiftBytes(len, copy=true) { 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) { - if (len === undefined) { len = this.rQlen; } // 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)); this._rQi += len; } - rQslice(start, end = this.rQlen) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + rQpeekBytes(len, copy=true) { + 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) // to be available in the receive queue. Return true if we need to // wait (and possibly print a debug message), otherwise false. rQwait(msg, num, goback) { - if (this.rQlen < num) { + if (this._rQlen - this._rQi < num) { if (goback) { if (this._rQi < goback) { throw new Error("rQwait cannot backup " + goback + " bytes"); @@ -190,21 +175,56 @@ export default class Websock { // 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() { 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; } } - send(arr) { - this._sQ.set(arr, this._sQlen); - this._sQlen += arr.length; - this.flush(); - } - - sendString(str) { - this.send(str.split('').map(chr => chr.charCodeAt(0))); + _sQensureSpace(bytes) { + if (this._sQbufferSize - this._sQlen < bytes) { + this.flush(); + } } // Event Handlers @@ -283,17 +303,12 @@ export default class Websock { } // 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, // e.g. compacting. // The function also expands the receive que if needed, and for // performance reasons we combine these two actions to avoid - // unneccessary copying. + // unnecessary copying. _expandCompactRQ(minFit) { // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place // instead of resizing @@ -309,7 +324,7 @@ export default class Websock { // we don't want to grow unboundedly if (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"); } } @@ -327,25 +342,22 @@ export default class Websock { } // push arraybuffer values onto the end of the receive que - _DecodeMessage(data) { - const u8 = new Uint8Array(data); + _recvMessage(e) { + 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) { this._expandCompactRQ(u8.length); } this._rQ.set(u8, this._rQlen); this._rQlen += u8.length; - } - _recvMessage(e) { - this._DecodeMessage(e.data); - if (this.rQlen > 0) { + if (this._rQlen - this._rQi > 0) { 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 { Log.Debug("Ignoring empty message"); } diff --git a/public/novnc/package.json b/public/novnc/package.json new file mode 100644 index 00000000..c032582d --- /dev/null +++ b/public/novnc/package.json @@ -0,0 +1 @@ +{ "version": "1.5.0" } \ No newline at end of file diff --git a/public/scripts/agent-desktop-0.0.2-min.js b/public/scripts/agent-desktop-0.0.2-min.js index 772ba774..9bfbf813 100644 --- a/public/scripts/agent-desktop-0.0.2-min.js +++ b/public/scripts/agent-desktop-0.0.2-min.js @@ -1 +1 @@ -function isWindowsBrowser(){return navigator&&!!/win/i.exec(navigator.platform)}Uint8Array.prototype.slice||Object.defineProperty(Uint8Array.prototype,"slice",{value:function(e,t){return new Uint8Array(Array.prototype.slice.call(this,e,t))}});var CreateAgentRemoteDesktop=function(e,t){var g={},p=("string"==typeof(g.CanvasId=e)&&(g.CanvasId=Q(e)),g.Canvas=g.CanvasId.getContext("2d"),g.scrolldiv=t,g.State=0,g.PendingOperations=[],g.tilesReceived=0,g.TilesDrawn=0,g.KillDraw=0,g.ipad=!1,g.tabletKeyboardVisible=!1,g.LastX=0,g.LastY=0,g.touchenabled=0,g.submenuoffset=0,g.touchtimer=null,g.TouchArray={},g.connectmode=0,g.connectioncount=0,g.rotation=0,g.protocol=2,g.debugmode=0,g.firstUpKeys=[],g.stopInput=!1,g.localKeyMap=!0,g.remoteKeyMap=!1,g.pressedKeys=[],g._altGrArmed=!1,g._altGrTimeout=0,g.isWindowsBrowser=isWindowsBrowser(),g.sessionid=0,g.oldie=!1,g.ImageType=1,g.CompressionLevel=50,g.ScalingLevel=1024,g.FrameRateTimer=100,g.SwapMouse=!1,g.UseExtendedKeyFlag=!0,g.FirstDraw=!1,g.onRemoteInputLockChanged=null,g.RemoteInputLock=null,g.onKeyboardStateChanged=null,g.KeyboardState=0,g.ScreenWidth=960,g.ScreenHeight=701,g.width=960,g.height=960,g.displays=null,g.selectedDisplay=null,g.onScreenSizeChange=null,g.onMessage=null,g.onConnectCountChanged=null,g.onDebugMessage=null,g.onTouchEnabledChanged=null,g.onDisplayinfo=null,!(g.accumulator=null)),S="default",v=(g.mouseCursorActive=function(e){p!=e&&(p=e,g.CanvasId.style.cursor=1==e?S:"default")},["default","progress","crosshair","pointer","help","text","no-drop","move","nesw-resize","ns-resize","nwse-resize","w-resize","alias","wait","none","not-allowed","col-resize","row-resize","copy","zoom-in","zoom-out"]),a=(g.Start=function(){g.State=0,g.accumulator=null},g.Stop=function(){g.setRotation(0),g.UnGrabKeyInput(),g.UnGrabMouseInput(),g.touchenabled=0,null!=g.onScreenSizeChange&&g.onScreenSizeChange(g,g.ScreenWidth,g.ScreenHeight,g.CanvasId),g.Canvas.clearRect(0,0,g.CanvasId.width,g.CanvasId.height)},g.xxStateChange=function(e){g.State!=e&&(g.State=e,g.CanvasId.style.cursor="default",0===e)&&g.Stop()},g.send=function(e){2>32)+g.intToStr(32&o)):(g.recordedSize+=n.length,g.shortToStr(e)+g.shortToStr(t)+g.intToStr(n.length)+g.intToStr(o>>32)+g.intToStr(32&o)+n)}return g.checkAltGr=function(e,t,n){return e._altGrArmed&&(e._altGrArmed=!1,clearTimeout(e._altGrTimeout),"AltRight"===t.code)&&t.timeStamp-e._altGrCtrlTime<50?(e.SendKeyMsgKC(n,225,!1),!0):!("ControlLeft"!==t.code||17 in e.pressedKeys||(e._altGrArmed=!0,e._altGrCtrlTime=t.timeStamp,1!=n)||(e._altGrTimeout=setTimeout(e._handleAltGrTimeout.bind(e),100),0))},g._handleAltGrTimeout=function(){g._altGrArmed=!1,clearTimeout(g._altGrTimeout),g.SendKeyMsgKC(1,17,!1)},g.SendRemoteInputLock=function(e){g.send(String.fromCharCode(0,87,0,5,e))},g.SendMessage=function(e){3==g.State&&g.send(String.fromCharCode(0,17)+g.shortToStr(4+e.length)+e)},g.SendKeyMsgKC=function(e,t,n){if(3==g.State)if("object"==typeof e)for(var o in e)g.SendKeyMsgKC(e[o][0],e[o][1],e[o][2]);else{1==e?-1==g.pressedKeys.indexOf(t)&&g.pressedKeys.unshift(t):2==e&&-1!=(o=g.pressedKeys.indexOf(t))&&g.pressedKeys.splice(o,1),0>8),255-(255&Math.abs(r))):(s=r>>8,255&r),String.fromCharCode(0,g.InputType.MOUSE,0,12,0,0,n/256&255,255&n,o/256&255,255&o,s,i)):String.fromCharCode(0,g.InputType.MOUSE,0,10,0,e==g.KeyAction.DOWN?a:2*a&255,n/256&255,255&n,o/256&255,255&o),g.Action==g.KeyAction.NONE?0==g.Alternate||g.ipad?(g.send(t),g.Alternate=1):g.Alternate=0:g.send(t))},g.GetDisplayNumbers=function(){g.send(String.fromCharCode(0,11,0,4))},g.SetDisplay=function(e){g.send(String.fromCharCode(0,12,0,6,e>>8,255&e))},g.intToStr=function(e){return String.fromCharCode(e>>24&255,e>>16&255,e>>8&255,255&e)},g.shortToStr=function(e){return String.fromCharCode(e>>8&255,255&e)},g.onResize=function(){0==g.ScreenWidth||0==g.ScreenHeight||g.Canvas.canvas.width==g.ScreenWidth&&g.Canvas.canvas.height==g.ScreenHeight||(g.FirstDraw&&(g.Canvas.canvas.width=g.ScreenWidth,g.Canvas.canvas.height=g.ScreenHeight,g.Canvas.fillRect(0,0,g.ScreenWidth,g.ScreenHeight),null!=g.onScreenSizeChange)&&g.onScreenSizeChange(g,g.ScreenWidth,g.ScreenHeight,g.CanvasId),g.FirstDraw=!1,1>32)+p.intToStr(32&o)):(p.recordedSize+=n.length,p.shortToStr(e)+p.shortToStr(t)+p.intToStr(n.length)+p.intToStr(o>>32)+p.intToStr(32&o)+n)}return p.checkAltGr=function(e,t,n){return e._altGrArmed&&(e._altGrArmed=!1,clearTimeout(e._altGrTimeout),"AltRight"===t.code)&&t.timeStamp-e._altGrCtrlTime<50?(e.SendKeyMsgKC(n,225,!1),!0):!("ControlLeft"!==t.code||17 in e.pressedKeys||(e._altGrArmed=!0,e._altGrCtrlTime=t.timeStamp,1!=n)||(e._altGrTimeout=setTimeout(e._handleAltGrTimeout.bind(e),100),0))},p._handleAltGrTimeout=function(){p._altGrArmed=!1,clearTimeout(p._altGrTimeout),p.SendKeyMsgKC(1,17,!1)},p.SendRemoteInputLock=function(e){p.send(String.fromCharCode(0,87,0,5,e))},p.SendMessage=function(e){3==p.State&&p.send(String.fromCharCode(0,17)+p.shortToStr(4+e.length)+e)},p.SendKeyMsgKC=function(e,t,n){if(3==p.State)if("object"==typeof e)for(var o in e)p.SendKeyMsgKC(e[o][0],e[o][1],e[o][2]);else{1==e?-1==p.pressedKeys.indexOf(t)&&p.pressedKeys.unshift(t):2==e&&-1!=(o=p.pressedKeys.indexOf(t))&&p.pressedKeys.splice(o,1),0>8),255-(255&Math.abs(r))):(s=r>>8,255&r),String.fromCharCode(0,p.InputType.MOUSE,0,12,0,0,n/256&255,255&n,o/256&255,255&o,s,i)):String.fromCharCode(0,p.InputType.MOUSE,0,10,0,e==p.KeyAction.DOWN?a:2*a&255,n/256&255,255&n,o/256&255,255&o),p.Action==p.KeyAction.NONE?0==p.Alternate||p.ipad?(p.send(t),p.Alternate=1):p.Alternate=0:p.send(t))},p.GetDisplayNumbers=function(){p.send(String.fromCharCode(0,11,0,4))},p.SetDisplay=function(e){p.send(String.fromCharCode(0,12,0,6,e>>8,255&e))},p.intToStr=function(e){return String.fromCharCode(e>>24&255,e>>16&255,e>>8&255,255&e)},p.shortToStr=function(e){return String.fromCharCode(e>>8&255,255&e)},p.onResize=function(){0==p.ScreenWidth||0==p.ScreenHeight||p.Canvas.canvas.width==p.ScreenWidth&&p.Canvas.canvas.height==p.ScreenHeight||(p.FirstDraw&&(p.Canvas.canvas.width=p.ScreenWidth,p.Canvas.canvas.height=p.ScreenHeight,p.Canvas.fillRect(0,0,p.ScreenWidth,p.ScreenHeight),null!=p.onScreenSizeChange)&&p.onScreenSizeChange(p,p.ScreenWidth,p.ScreenHeight,p.CanvasId),p.FirstDraw=!1,1 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 if (obj.recordedData != null) { 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 { - 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; 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))); } - 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) { if (event.detail) { Delta = (-1 * (event.detail * 120)); } else if (event.wheelDelta) { Delta = (event.wheelDelta * 3); } diff --git a/public/scripts/agent-rdp-0.0.1-min.js b/public/scripts/agent-rdp-0.0.1-min.js index c8f14c59..dc3b7f4b 100644 --- a/public/scripts/agent-rdp-0.0.1-min.js +++ b/public/scripts/agent-rdp-0.0.1-min.js @@ -1 +1 @@ -var CreateRDPDesktop=function(e,a){var o={m:{KeyAction:{NONE:0,DOWN:1,UP:2,SCROLL:3,EXUP:4,EXDOWN:5,DBLCLICK:6}},State:0},i=(o.canvas=Q(e),"string"==typeof(o.CanvasId=e)&&(o.CanvasId=Q(e)),o.Canvas=o.CanvasId.getContext("2d"),o.ScreenWidth=o.width=1280,o.ScreenHeight=o.height=1024,o.m.onClipboardChanged=null,!(o.onConsoleMessageChange=null)),r="default";function n(e){return(!0===o.m.SwapMouse?[2,0,1,0,0]:[1,0,2,0,0])[e]}function c(e){o.State!=e&&(o.State=e,null!=o.onStateChanged)&&o.onStateChanged(o,o.State)}function s(e){var t=o.Canvas.canvas.height/o.CanvasId.clientHeight,n=o.Canvas.canvas.width/o.CanvasId.clientWidth,s=function(e){var t=Array(2);for(t[0]=t[1]=0;e;)t[0]+=e.offsetLeft,t[1]+=e.offsetTop,e=e.offsetParent;return t}(o.Canvas.canvas),n=(e.pageX-s[0])*n,s=(e.pageY-s[1])*t;return e.addx&&(n+=e.addx),e.addy&&(s+=e.addy),{x:n,y:s}}o.mouseCursorActive=function(e){i!=e&&(i=e,o.CanvasId.style.cursor=1==e?r:"default")},o.Start=function(e,t,n){c(1),o.nodeid=e,o.port=t;var s={savepass:(o.credentials=n).savecred,useServerCreds:n.servercred,width:n.width,height:n.height,flags:n.flags,workingDir:n.workdir,alternateShell:n.altshell};n.width&&n.height&&(s.width=o.ScreenWidth=o.width=n.width,s.height=o.ScreenHeight=o.height=n.height,delete n.width,delete n.height),o.render=new Mstsc.Canvas.create(o.canvas),o.socket=new WebSocket("wss://"+window.location.host+a+"mstscrelay.ashx"),o.socket.binaryType="arraybuffer",o.socket.onopen=function(){c(2),o.socket.send(JSON.stringify(["infos",{ip:o.nodeid,port:o.port,screen:{width:o.width,height:o.height},domain:n.domain,username:n.username,password:n.password,options:s,locale:Mstsc.locale()}]))},o.socket.onmessage=function(e){if("string"==typeof e.data){var t=JSON.parse(e.data);switch(t[0]){case"rdp-connect":c(3),o.rotation=0,o.Canvas.setTransform(1,0,0,1,0,0),o.Canvas.canvas.width=o.ScreenWidth,o.Canvas.canvas.height=o.ScreenHeight,o.Canvas.fillRect(0,0,o.ScreenWidth,o.ScreenHeight),null!=o.m.onScreenSizeChange&&o.m.onScreenSizeChange(o,o.ScreenWidth,o.ScreenHeight,o.CanvasId);break;case"rdp-bitmap":null!=o.bitmapData&&((n=t[1]).data=o.bitmapData,delete o.bitmapData,o.render.update(n));break;case"rdp-pointer":var n=t[1];r=n,i&&(o.CanvasId.style.cursor=n);break;case"rdp-close":o.Stop();break;case"rdp-error":switch(o.consoleMessageTimeout=5,o.consoleMessage=t[1],delete o.consoleMessageArgs,2o.ScreenWidth||t.y>o.ScreenHeight))return o.mouseNagleData=["mouse",t.x,t.y,0,!1],null==o.mouseNagleTimer&&(o.mouseNagleTimer=setTimeout(function(){o.socket.send(JSON.stringify(o.mouseNagleData)),o.mouseNagleTimer=null},50)),e.preventDefault(),!1}},o.m.mouseup=function(e){if(o.socket&&3==o.State){var t=s(e);if(!(t.x<0||t.y<0||t.x>o.ScreenWidth||t.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),o.socket.send(JSON.stringify(["mouse",t.x,t.y,n(e.button),!1])),e.preventDefault(),!1}},o.m.mousedown=function(e){if(o.socket&&3==o.State){var t=s(e);if(!(t.x<0||t.y<0||t.x>o.ScreenWidth||t.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),o.socket.send(JSON.stringify(["mouse",t.x,t.y,n(e.button),!0])),e.preventDefault(),!1}},o.m.handleKeyUp=function(e){if(o.socket&&3==o.State)return o.socket.send(JSON.stringify(["scancode",Mstsc.scancode(e),!1])),e.preventDefault(),!1},o.m.handleKeyDown=function(e){if(o.socket&&3==o.State)return o.socket.send(JSON.stringify(["scancode",Mstsc.scancode(e),!0])),e.preventDefault(),!1},o.m.mousewheel=function(e){if(o.socket&&3==o.State){var t,n=s(e);if(!(n.x<0||n.y<0||n.x>o.ScreenWidth||n.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),t=0,e.detail?t=120*e.detail:e.wheelDelta&&(t=3*e.wheelDelta),o.m.ReverseMouseWheel&&(t*=-1),0!=t&&o.socket.send(JSON.stringify(["wheel",n.x,n.y,t,!1,!1])),e.preventDefault(),!1}},o.m.SendStringUnicode=function(e){o.socket&&3==o.State&&o.socket.send(JSON.stringify(["utype",e]))},o.m.SendKeyMsgKC=function(e,t,n){if(3==o.State)if("object"==typeof e)for(var s in e)o.m.SendKeyMsgKC(e[s][0],e[s][1],e[s][2]);else{t=d[t];null!=t&&o.socket.send(JSON.stringify(["scancode",t,0!=(1&e)]))}},o.m.mousedblclick=function(){},o.m.handleKeyPress=function(){},o.m.setRotation=function(){},o.m.sendcad=function(){o.socket.send(JSON.stringify(["scancode",29,!0])),o.socket.send(JSON.stringify(["scancode",56,!0])),o.socket.send(JSON.stringify(["scancode",57427,!0])),o.socket.send(JSON.stringify(["scancode",57427,!1])),o.socket.send(JSON.stringify(["scancode",56,!1])),o.socket.send(JSON.stringify(["scancode",29,!1]))};var d={9:15,16:42,17:29,18:56,27:1,33:57417,34:57425,35:57423,36:57415,37:57419,38:57416,39:57421,40:57424,44:57399,45:57426,46:57427,65:30,66:48,67:46,68:32,69:18,70:33,71:34,72:35,73:23,74:36,75:37,76:38,77:50,78:49,79:24,80:25,81:16,82:19,83:31,84:20,85:22,86:47,87:17,88:45,89:21,90:44,91:57435,112:59,113:60,114:61,115:62,116:63,117:64,118:65,119:66,120:67,121:68,122:87,123:88};return o} \ No newline at end of file +var CreateRDPDesktop=function(e,a){var o={m:{KeyAction:{NONE:0,DOWN:1,UP:2,SCROLL:3,EXUP:4,EXDOWN:5,DBLCLICK:6}},State:0},i=(o.canvas=Q(e),"string"==typeof(o.CanvasId=e)&&(o.CanvasId=Q(e)),o.Canvas=o.CanvasId.getContext("2d"),o.ScreenWidth=o.width=1280,o.ScreenHeight=o.height=1024,o.m.onClipboardChanged=null,!(o.onConsoleMessageChange=null)),r="default";function n(e){return(!0===o.m.SwapMouse?[2,0,1,0,0]:[1,0,2,0,0])[e]}function c(e){o.State!=e&&(o.State=e,null!=o.onStateChanged)&&o.onStateChanged(o,o.State)}function s(e){var t=o.Canvas.canvas.height/o.CanvasId.clientHeight,n=o.Canvas.canvas.width/o.CanvasId.clientWidth,s=(e=>{var t=Array(2);for(t[0]=t[1]=0;e;)t[0]+=e.offsetLeft,t[1]+=e.offsetTop,e=e.offsetParent;return t})(o.Canvas.canvas),n=(e.pageX-s[0])*n,s=(e.pageY-s[1])*t;return e.addx&&(n+=e.addx),e.addy&&(s+=e.addy),{x:n,y:s}}o.mouseCursorActive=function(e){i!=e&&(i=e,o.CanvasId.style.cursor=1==e?r:"default")},o.Start=function(e,t,n){c(1),o.nodeid=e,o.port=t;var s={savepass:(o.credentials=n).savecred,useServerCreds:n.servercred,width:n.width,height:n.height,flags:n.flags,workingDir:n.workdir,alternateShell:n.altshell};n.width&&n.height&&(s.width=o.ScreenWidth=o.width=n.width,s.height=o.ScreenHeight=o.height=n.height,delete n.width,delete n.height),o.render=new Mstsc.Canvas.create(o.canvas),o.socket=new WebSocket("wss://"+window.location.host+a+"mstscrelay.ashx"),o.socket.binaryType="arraybuffer",o.socket.onopen=function(){c(2),o.socket.send(JSON.stringify(["infos",{ip:o.nodeid,port:o.port,screen:{width:o.width,height:o.height},domain:n.domain,username:n.username,password:n.password,options:s,locale:Mstsc.locale()}]))},o.socket.onmessage=function(e){if("string"==typeof e.data){var t=JSON.parse(e.data);switch(t[0]){case"rdp-connect":c(3),o.rotation=0,o.Canvas.setTransform(1,0,0,1,0,0),o.Canvas.canvas.width=o.ScreenWidth,o.Canvas.canvas.height=o.ScreenHeight,o.Canvas.fillRect(0,0,o.ScreenWidth,o.ScreenHeight),null!=o.m.onScreenSizeChange&&o.m.onScreenSizeChange(o,o.ScreenWidth,o.ScreenHeight,o.CanvasId);break;case"rdp-bitmap":null!=o.bitmapData&&((n=t[1]).data=o.bitmapData,delete o.bitmapData,o.render.update(n));break;case"rdp-pointer":var n=t[1];r=n,i&&(o.CanvasId.style.cursor=n);break;case"rdp-close":o.Stop();break;case"rdp-error":switch(o.consoleMessageTimeout=5,o.consoleMessage=t[1],delete o.consoleMessageArgs,2o.ScreenWidth||t.y>o.ScreenHeight))return o.mouseNagleData=["mouse",t.x,t.y,0,!1],null==o.mouseNagleTimer&&(o.mouseNagleTimer=setTimeout(function(){o.socket.send(JSON.stringify(o.mouseNagleData)),o.mouseNagleTimer=null},50)),e.preventDefault(),!1}},o.m.mouseup=function(e){if(o.socket&&3==o.State){var t=s(e);if(!(t.x<0||t.y<0||t.x>o.ScreenWidth||t.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),o.socket.send(JSON.stringify(["mouse",t.x,t.y,n(e.button),!1])),e.preventDefault(),!1}},o.m.mousedown=function(e){if(o.socket&&3==o.State){var t=s(e);if(!(t.x<0||t.y<0||t.x>o.ScreenWidth||t.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),o.socket.send(JSON.stringify(["mouse",t.x,t.y,n(e.button),!0])),e.preventDefault(),!1}},o.m.handleKeyUp=function(e){if(o.socket&&3==o.State)return o.socket.send(JSON.stringify(["scancode",Mstsc.scancode(e),!1])),e.preventDefault(),!1},o.m.handleKeyDown=function(e){if(o.socket&&3==o.State)return o.socket.send(JSON.stringify(["scancode",Mstsc.scancode(e),!0])),e.preventDefault(),!1},o.m.mousewheel=function(e){if(o.socket&&3==o.State){var t,n=s(e);if(!(n.x<0||n.y<0||n.x>o.ScreenWidth||n.y>o.ScreenHeight))return null!=o.mouseNagleTimer&&(clearTimeout(o.mouseNagleTimer),o.mouseNagleTimer=null),t=0,e.detail?t=120*e.detail:e.wheelDelta&&(t=3*e.wheelDelta),o.m.ReverseMouseWheel&&(t*=-1),0!=t&&o.socket.send(JSON.stringify(["wheel",n.x,n.y,t,!1,!1])),e.preventDefault(),!1}},o.m.SendStringUnicode=function(e){o.socket&&3==o.State&&o.socket.send(JSON.stringify(["utype",e]))},o.m.SendKeyMsgKC=function(e,t,n){if(3==o.State)if("object"==typeof e)for(var s in e)o.m.SendKeyMsgKC(e[s][0],e[s][1],e[s][2]);else{t=d[t];null!=t&&o.socket.send(JSON.stringify(["scancode",t,0!=(1&e)]))}},o.m.mousedblclick=function(){},o.m.handleKeyPress=function(){},o.m.setRotation=function(){},o.m.sendcad=function(){o.socket.send(JSON.stringify(["scancode",29,!0])),o.socket.send(JSON.stringify(["scancode",56,!0])),o.socket.send(JSON.stringify(["scancode",57427,!0])),o.socket.send(JSON.stringify(["scancode",57427,!1])),o.socket.send(JSON.stringify(["scancode",56,!1])),o.socket.send(JSON.stringify(["scancode",29,!1]))};var d={9:15,16:42,17:29,18:56,27:1,33:57417,34:57425,35:57423,36:57415,37:57419,38:57416,39:57421,40:57424,44:57399,45:57426,46:57427,65:30,66:48,67:46,68:32,69:18,70:33,71:34,72:35,73:23,74:36,75:37,76:38,77:50,78:49,79:24,80:25,81:16,82:19,83:31,84:20,85:22,86:47,87:17,88:45,89:21,90:44,91:57435,112:59,113:60,114:61,115:62,116:63,117:64,118:65,119:66,120:67,121:68,122:87,123:88};return o} \ No newline at end of file diff --git a/public/scripts/agent-redir-ws-0.1.1-min.js b/public/scripts/agent-redir-ws-0.1.1-min.js index 89bbc732..d7f32b38 100644 --- a/public/scripts/agent-redir-ws-0.1.1-min.js +++ b/public/scripts/agent-redir-ws-0.1.1-min.js @@ -1 +1 @@ -var CreateAgentRedirect=function(e,t,n,o,a,c){var s={};function l(){1==s.webSwitchOk&&1==s.webRtcActive&&(s.latency.current=-1,s.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc0"}'),s.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc1"}'),null!=s.onStateChanged)&&s.onStateChanged(s,s.State)}((s.m=t).parent=s).meshserver=e,s.authCookie=o,s.rauthCookie=a,s.State=0,s.nodeid=null,s.options=null,s.socket=null,s.connectstate=-1,s.tunnelid=Math.random().toString(36).substring(2),s.protocol=t.protocol,s.onStateChanged=null,s.ctrlMsgAllowed=!0,s.attemptWebRTC=!1,s.webRtcActive=!1,s.webSwitchOk=!1,s.webchannel=null,s.webrtc=null,s.debugmode=0,s.serverIsRecording=!1,s.urlname="meshrelay.ashx",s.latency={lastSend:null,current:-1,callback:null},null==c&&(c="/"),s.consoleMessage=null,s.onConsoleMessageChange=null,s.metadata=null,s.onMetadataChange=null,s.Start=function(e){var t=window.location.protocol.replace("http","ws")+"//"+window.location.host+window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"+s.urlname+"?browser=1&p="+s.protocol+(e?"&nodeid="+e:"")+"&id="+s.tunnelid;null!=o&&""!=o&&(t+="&auth="+o),null!=urlargs&&null!=urlargs.slowrelay&&(t+="&slowrelay="+urlargs.slowrelay),s.nodeid=e,s.connectstate=0,s.socket=new WebSocket(t),s.socket.binaryType="arraybuffer",s.socket.onopen=s.xxOnSocketConnected,s.socket.onmessage=s.xxOnMessage,s.socket.onerror=function(e){},s.socket.onclose=s.xxOnSocketClosed,s.xxStateChange(1),null!=s.meshserver&&(t="*"+c+"meshrelay.ashx?p="+s.protocol+"&nodeid="+e+"&id="+s.tunnelid,null!=a&&""!=a&&(t+="&rauth="+a),s.meshserver.send({action:"msg",type:"tunnel",nodeid:s.nodeid,value:t,usage:s.protocol}))},s.xxOnSocketConnected=function(){1==s.debugmode&&console.log("onSocketConnected"),s.xxStateChange(2)},s.xxOnControlCommand=function(e){var t;try{t=JSON.parse(e)}catch(e){return}"102938"!=t.ctrlChannel?s.m.ProcessData?s.m.ProcessData(e):console.log(e):("undefined"!=typeof args&&args.redirtrace&&console.log("RedirRecv",t),"console"==t.type?s.setConsoleMessage(t.msg,t.msgid,t.msgargs,t.timeout):"metadata"==t.type?(s.metadata=t,s.onMetadataChange&&s.onMetadataChange(s.metadata)):"rtt"==t.type&&"number"==typeof t.time?(s.latency.current=(new Date).getTime()-t.time,null!=s.latency.callbacks&&s.latency.callback(s.latency.current)):null!=s.webrtc?"answer"==t.type?s.webrtc.setRemoteDescription(new RTCSessionDescription(t),function(){},s.xxCloseWebRTC):"webrtc0"==t.type?(s.webSwitchOk=!0,l()):"webrtc1"==t.type?s.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc2"}'):t.type:"ping"==t.type&&s.sendCtrlMsg('{"ctrlChannel":"102938","type":"pong"}'))},s.setConsoleMessage=function(e,t,n,o){s.consoleMessage!=e&&(s.consoleMessage=e,s.consoleMessageId=t,s.consoleMessageArgs=n,s.consoleMessageTimeout=o,s.onConsoleMessageChange)&&s.onConsoleMessageChange(s,s.consoleMessage,s.consoleMessageId)},s.sendCtrlMsg=function(e){if(1==s.ctrlMsgAllowed){"undefined"!=typeof args&&args.redirtrace&&console.log("RedirSend",typeof e,e);try{s.socket.send(e)}catch(e){}}},s.xxOnMessage=function(e){if(s.State<3&&("c"==e.data||"cr"==e.data)){if("cr"==e.data&&(s.serverIsRecording=!0),null!=s.options){delete s.options.action,s.options.type="options";try{s.sendCtrlMsg(JSON.stringify(s.options))}catch(e){}}try{s.socket.send(s.protocol)}catch(e){}s.xxStateChange(3),1==s.attemptWebRTC&&("undefined"!=typeof RTCPeerConnection?s.webrtc=new RTCPeerConnection(null):"undefined"!=typeof webkitRTCPeerConnection&&(s.webrtc=new webkitRTCPeerConnection(null)),null!=s.webrtc)&&s.webrtc.createDataChannel&&(s.webchannel=s.webrtc.createDataChannel("DataChannel",{}),s.webchannel.binaryType="arraybuffer",s.webchannel.onmessage=s.xxOnMessage,s.webchannel.onopen=function(){s.webRtcActive=!0,l()},s.webchannel.onclose=function(e){s.webRtcActive&&s.Stop()},s.webrtc.onicecandidate=function(e){if(null==e.candidate)try{s.sendCtrlMsg(JSON.stringify(s.webrtcoffer))}catch(e){}else s.webrtcoffer.sdp+="a="+e.candidate.candidate+"\r\n"},s.webrtc.oniceconnectionstatechange=function(){null!=s.webrtc&&("disconnected"==s.webrtc.iceConnectionState?1==s.webRtcActive?s.Stop():s.xxCloseWebRTC():"failed"==s.webrtc.iceConnectionState&&s.xxCloseWebRTC())},s.webrtc.createOffer(function(e){s.webrtcoffer=e,s.webrtc.setLocalDescription(e,function(){},s.xxCloseWebRTC)},s.xxCloseWebRTC,{mandatory:{OfferToReceiveAudio:!1,OfferToReceiveVideo:!1}}))}else if("string"==typeof e.data)"~"==e.data[0]?s.m.ProcessData(e.data):s.xxOnControlCommand(e.data);else if(s.m.ProcessBinaryCommand){if(!(0==u&&e.data.byteLength<4))if(0!=u){var t=new Uint8Array(e.data);if(g.push(t),u+=t.byteLength,d<=u){var n,o=new Uint8Array(u),a=0;for(n in g)o.set(g[n],a),a+=g[n].byteLength;s.m.ProcessBinaryCommand(i,d,o),u=d=i=0,g=[]}}else{var c=((t=new Uint8Array(e.data))[0]<<8)+t[1],r=(t[2]<<8)+t[3];27==c&&8==r&&(c=(t[8]<<8)+t[9],r=(t[5]<<16)+(t[6]<<8)+t[7],t=t.slice(8)),r!=t.byteLength?(i=c,d=r,u=t.byteLength,g=[t]):s.m.ProcessBinaryCommand(c,r,t)}}else s.m.ProcessBinaryData?s.m.ProcessBinaryData(new Uint8Array(e.data)):e.data.byteLength<16e3?s.m.ProcessData(String.fromCharCode.apply(null,new Uint8Array(e.data))):(c=new Blob([new Uint8Array(e.data)]),(r=new FileReader).onload=function(e){s.m.ProcessData(e.target.result)},r.readAsBinaryString(c))};var i=0,d=0,u=0,g=[];return s.sendText=function(e){"string"!=typeof e&&(e=JSON.stringify(e)),s.send(encode_utf8(e))},s.send=function(e){"undefined"!=typeof args&&args.redirtrace&&console.log("RedirSend",typeof e,e.length,"{"==e[0]?e:rstr2hex(e).substring(0,64));try{if(null!=s.socket&&s.socket.readyState==WebSocket.OPEN)if("string"==typeof e){if(1==s.debugmode)for(var t=new Uint8Array(e.length),n=[],o=0;o>8&248)+","+(c>>3&252)+","+((31&c)<<3))+")");var f=k(a);n=x(0,n),S.canvas.fillRect(a=f,n,r,o)}else if(1>d&p],u++)}else{for(d=0;d>d&p],u++)}w(S.spare,a,n)}else if(128==l){if(2==S.bpp)for(;u>8&248,S.spare.data[r+1]=e>>3&252,S.spare.data[r+2]=(31&e)<<3}function b(e,t,a){if(S.graymode){var n=t<<2;for(S.lowcolor&&(e<<=4);0<=--a;)S.spare.data[n]=S.spare.data[n+1]=S.spare.data[n+2]=e,n+=4}else for(var n=t<<2,r=224&e,o=(28&e)<<3,i=T((3&e)<<6);0<=--a;)S.spare.data[n]=r,S.spare.data[n+1]=o,S.spare.data[n+2]=i,n+=4}function D(e,t,a){for(var n=t<<2,r=e>>8&248,o=e>>3&252,i=(31&e)<<3;0<=--a;)S.spare.data[n]=r,S.spare.data[n+1]=o,S.spare.data[n+2]=i,n+=4}function k(e){return 0==S.rotation||1==S.rotation?e:2==S.rotation?e-S.canvas.canvas.width:3==S.rotation?e-S.canvas.canvas.height:0}function x(e,t){return 0==S.rotation?t:1==S.rotation?t-S.canvas.canvas.width:2==S.rotation?t-S.canvas.canvas.height:3==S.rotation?t:0}function T(e){return 127>32)+IntToStr(32&n)):(S.recordedSize+=a.length,ShortToStr(e)+ShortToStr(t)+IntToStr(a.length)+IntToStr(n>>32)+IntToStr(32&n)+a)}return S.GrabMouseInput=function(){var e;1!=n&&((e=S.canvas.canvas).onmouseup=S.mouseup,e.onmousedown=S.mousedown,e.onmousemove=S.mousemove,e.onwheel=S.mousewheel,n=!0)},S.UnGrabMouseInput=function(){var e;0!=n&&((e=S.canvas.canvas).onmousemove=null,e.onmouseup=null,e.onmousedown=null,e.onwheel=null,n=!1)},S.GrabKeyInput=function(){1!=o&&(document.onkeyup=S.handleKeyUp,document.onkeydown=S.handleKeyDown,document.onkeypress=S.handleKeys,o=!0)},S.UnGrabKeyInput=function(){0!=o&&(document.onkeyup=null,document.onkeydown=null,document.onkeypress=null,o=!1)},S.handleKeys=function(e){return S.haltEvent(e)},S.handleKeyUp=function(e){return a(0,e)},S.handleKeyDown=function(e){return a(1,e)},S.haltEvent=function(e){return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),!1},S.mousedblclick=function(e){},S.mousewheel=function(e){var t,a=0;if("number"==typeof e.deltaY?a=-1*e.deltaY:"number"==typeof e.detail?a=-1*e.detail:"number"==typeof e.wheelDelta&&(a=e.wheelDelta),0!=a)return S.ReverseMouseWheel&&(a*=-1),t=S.buttonmask,S.buttonmask|=1<<(0>8,255&S.width,S.height>>8,255&S.height)+S.DeskRecordServerInit.substring(4),S.recordedData.push(I(2,1,S.DeskRecordServerInit)),S.recordedData.push(I(3,0,atob(S.CanvasId.toDataURL("image/png").split(",")[1]))),!0)},S.StopRecording=function(){var e;if(null!=S.recordedData)return(e=S.recordedData).push(I(3,0,"MeshCentralMCREC")),delete S.recordedData,delete S.recordedStart,delete S.recordedSize,e},S} \ No newline at end of file +var CreateAmtRemoteDesktop=function(e,t){var S={};function g(e){return String.fromCharCode.apply(null,e)}function p(e,t,a,n,r,o,i){var s,c,h,d,l=e[t++],v={},u=0,m=0;if(0==l){if(2==S.bpp)for(d=0;d>8&248)+","+(c>>3&252)+","+((31&c)<<3))+")");var f=k(a);n=x(0,n),S.canvas.fillRect(a=f,n,r,o)}else if(1>d&p],u++)}else{for(d=0;d>d&p],u++)}w(S.spare,a,n)}else if(128==l){if(2==S.bpp)for(;u>8&248,S.spare.data[r+1]=e>>3&252,S.spare.data[r+2]=(31&e)<<3}function b(e,t,a){if(S.graymode){var n=t<<2;for(S.lowcolor&&(e<<=4);0<=--a;)S.spare.data[n]=S.spare.data[n+1]=S.spare.data[n+2]=e,n+=4}else for(var n=t<<2,r=224&e,o=(28&e)<<3,i=T((3&e)<<6);0<=--a;)S.spare.data[n]=r,S.spare.data[n+1]=o,S.spare.data[n+2]=i,n+=4}function D(e,t,a){for(var n=t<<2,r=e>>8&248,o=e>>3&252,i=(31&e)<<3;0<=--a;)S.spare.data[n]=r,S.spare.data[n+1]=o,S.spare.data[n+2]=i,n+=4}function k(e){return 0==S.rotation||1==S.rotation?e:2==S.rotation?e-S.canvas.canvas.width:3==S.rotation?e-S.canvas.canvas.height:0}function x(e,t){return 0==S.rotation?t:1==S.rotation?t-S.canvas.canvas.width:2==S.rotation?t-S.canvas.canvas.height:3==S.rotation?t:0}function T(e){return 127{if(e.byteLength<8)return 0;if(t=t.getUint32(4)+8,e.byteLength{for(var t=new Uint8Array(e.length),a=0,n=e.length;a>32)+IntToStr(32&n)):(S.recordedSize+=a.length,ShortToStr(e)+ShortToStr(t)+IntToStr(a.length)+IntToStr(n>>32)+IntToStr(32&n)+a)}return S.GrabMouseInput=function(){var e;1!=n&&((e=S.canvas.canvas).onmouseup=S.mouseup,e.onmousedown=S.mousedown,e.onmousemove=S.mousemove,e.onwheel=S.mousewheel,n=!0)},S.UnGrabMouseInput=function(){var e;0!=n&&((e=S.canvas.canvas).onmousemove=null,e.onmouseup=null,e.onmousedown=null,e.onwheel=null,n=!1)},S.GrabKeyInput=function(){1!=o&&(document.onkeyup=S.handleKeyUp,document.onkeydown=S.handleKeyDown,document.onkeypress=S.handleKeys,o=!0)},S.UnGrabKeyInput=function(){0!=o&&(document.onkeyup=null,document.onkeydown=null,document.onkeypress=null,o=!1)},S.handleKeys=function(e){return S.haltEvent(e)},S.handleKeyUp=function(e){return a(0,e)},S.handleKeyDown=function(e){return a(1,e)},S.haltEvent=function(e){return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),!1},S.mousedblclick=function(e){},S.mousewheel=function(e){var t,a=0;if("number"==typeof e.deltaY?a=-1*e.deltaY:"number"==typeof e.detail?a=-1*e.detail:"number"==typeof e.wheelDelta&&(a=e.wheelDelta),0!=a)return S.ReverseMouseWheel&&(a*=-1),t=S.buttonmask,S.buttonmask|=1<<(0>8,255&S.width,S.height>>8,255&S.height)+S.DeskRecordServerInit.substring(4),S.recordedData.push(I(2,1,S.DeskRecordServerInit)),S.recordedData.push(I(3,0,atob(S.CanvasId.toDataURL("image/png").split(",")[1]))),!0)},S.StopRecording=function(){var e;if(null!=S.recordedData)return(e=S.recordedData).push(I(3,0,"MeshCentralMCREC")),delete S.recordedData,delete S.recordedStart,delete S.recordedSize,e},S} \ No newline at end of file diff --git a/public/scripts/amt-ider-ws-0.0.1-min.js b/public/scripts/amt-ider-ws-0.0.1-min.js index 9abe7730..9a4b6f5b 100644 --- a/public/scripts/amt-ider-ws-0.0.1-min.js +++ b/public/scripts/amt-ider-ws-0.0.1-min.js @@ -1 +1 @@ -var CreateAmtRemoteIder=function(){var m={};function l(){urlvars&&urlvars.idertrace&&console.log(...arguments)}m.protocol=3,m.bytesToAmt=0,m.bytesFromAmt=0,m.rx_timeout=3e4,m.tx_timeout=0,m.heartbeat=2e4,m.version=1,m.acc="",m.inSequence=0,m.outSequence=0,m.iderinfo=null,m.enabled=!1,m.iderStart=0,m.floppy=null,m.cdrom=null,m.floppyReady=!1,m.cdromReady=!1,m.pingTimer=null;var f=String.fromCharCode(0,38,49,128,0,0,0,0,5,30,16,169,8,32,2,0,3,195,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0),u=String.fromCharCode(0,92,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0,3,22,0,160,0,0,0,0,0,18,2,0,0,0,0,0,0,0,160,0,0,0,5,30,16,169,8,32,2,0,3,195,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0,8,10,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,17,36,49),h=String.fromCharCode(0,38,36,128,0,0,0,0,5,30,4,176,2,18,2,0,0,80,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0),p=String.fromCharCode(0,92,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0,3,22,0,160,0,0,0,0,0,18,2,0,0,0,0,0,0,0,160,0,0,0,5,30,4,176,2,18,2,0,0,80,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0,8,10,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,17,36,49),R=String.fromCharCode(0,18,1,128,0,0,0,0,26,10,0,0,0,0,0,0,0,0,0,0),E=String.fromCharCode(0,18,1,128,0,0,0,0,29,10,0,0,0,0,0,0,0,0,0,0),I=String.fromCharCode(0,32,1,128,0,0,0,0,42,24,0,0,0,0,32,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0),g=String.fromCharCode(0,40,1,128,0,0,0,0,1,6,0,255,0,0,0,0,42,24,0,0,0,0,2,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0),b=(String.fromCharCode(0,0,0,40,0,0,0,8),String.fromCharCode(0,0,3,4,0,8,1,0)),A=String.fromCharCode(0,1,3,4,0,0,0,2),T=String.fromCharCode(0,2,3,4,0,0,0,0),D=String.fromCharCode(0,3,3,4,41,0,0,2),y=String.fromCharCode(0,16,1,8,0,0,8,0,0,1,0,0),_=String.fromCharCode(0,30,3,0),k=String.fromCharCode(1,0,3,0),v=String.fromCharCode(1,5,3,0),O=String.fromCharCode(0,18,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0),X=String.fromCharCode(0,18,49,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0),w=String.fromCharCode(0,14,1,128,0,0,0,0,1,6,0,255,0,0,0,0);function F(e,r,n,a){var o=null,t=0;160==e&&(o=m.floppy,null!=m.floppy)&&(t=m.floppy.size>>9),176==e&&(o=m.cdrom,null!=m.cdrom)&&(t=m.cdrom.size>>11),n<0||tm.iderinfo.readbfr&&(n=m.iderinfo.readbfr);c-=n;S+=n;var o=new FileReader;o.onload=function(){m.SendDataToHost(d,0==c,this.result,1&r),0>8,0,a?180:181,0,2,0,255&o,o>>8,e,88,133,0,3,0,0,0,e,80,0,0,0,0,0,0)+n,r,a):m.SendCommand(84,String.fromCharCode(0,255&n.length,n.length>>8,0,a?180:181,0,2,0,255&o,o>>8,e,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0)+n,r,a)},m.SendGetDataFromHost=function(e,r){m.SendCommand(82,String.fromCharCode(0,255&r,r>>8,0,181,0,0,0,255&r,r>>8,e,88,0,0,0,0,0,0,0,0,0,0,0),!1)},m.SendDisableEnableFeatures=function(e,r){null==r&&(r=""),m.SendCommand(72,String.fromCharCode(e)+r)};var d,S,c,C=!(m.ProcessDataEx=function(){if(!(m.acc.length<8))switch(m.acc.charCodeAt(0)){case 65:return m.acc.length<30?0:(t=m.acc.charCodeAt(29),m.acc.length<30+t?0:(m.iderinfo={},m.iderinfo.major=m.acc.charCodeAt(8),m.iderinfo.minor=m.acc.charCodeAt(9),m.iderinfo.fwmajor=m.acc.charCodeAt(10),m.iderinfo.fwminor=m.acc.charCodeAt(11),m.iderinfo.readbfr=ReadShortX(m.acc,16),m.iderinfo.writebfr=ReadShortX(m.acc,18),m.iderinfo.proto=m.acc.charCodeAt(21),m.iderinfo.iana=ReadIntX(m.acc,25),l(m.iderinfo),0!=m.iderinfo.proto&&(l("Unknown proto",m.iderinfo.proto),m.Stop()),8192>9)-1:S);break;case 176:if(null==m.floppy||0==m.floppy.size)return m.SendCommandEndResponse(0,2,e,58,0);l("DEV_CDDVD",S=null!=m.cdrom?(m.cdrom.size>>11)-1:S);break;default:return l("SCSI Internal error 4",e)}l("SCSI: READ_CAPACITY2",e,a),m.SendDataToHost(a,!0,IntToStr(S)+String.fromCharCode(0,0,176==e?8:2,0),1&n);break;case 40:c=ReadInt(r,2),S=ReadShort(r,7),l("SCSI: READ_10",e,c,S),F(e,c,S,n);break;case 42:case 46:c=ReadInt(r,2),S=ReadShort(r,7),l("SCSI: WRITE_10",e,c,S),m.SendGetDataFromHost(e,512*S);break;case 67:var d=ReadShort(r,7),c=2&r.charCodeAt(1),C=7&r.charCodeAt(2);switch(0==C&&(C=r.charCodeAt(9)>>6),l("SCSI: READ_TOC, dev="+e+", buflen="+d+", msf="+c+", format="+C),e){case 160:return m.SendCommandEndResponse(1,5,e,32,0);case 176:break;default:return l("SCSI Internal error 9",e)}1==C?m.SendDataToHost(e,!0,String.fromCharCode(0,10,1,1,0,20,1,0,0,0,0,0),1&n):0==C&&(c?m.SendDataToHost(e,!0,String.fromCharCode(0,18,1,1,0,20,1,0,0,0,2,0,0,20,170,0,0,0,52,19),1&n):m.SendDataToHost(e,!0,String.fromCharCode(0,18,1,1,0,20,1,0,0,0,0,0,0,20,170,0,0,0,0,0),1&n));break;case 70:C=2!=r.charCodeAt(1),c=ReadShort(r,2),d=ReadShort(r,7);return l("SCSI: GET_CONFIGURATION",e,C,c,d),0==d?m.SendDataToHost(e,!0,IntToStr(60)+IntToStr(8),1&n):(s=IntToStr(8),0==c&&(s+=b),(1==c||C&&c<1)&&(s+=A),(2==c||C&&c<2)&&(s+=T),(3==c||C&&c<3)&&(s+=D),(16==c||C&&c<16)&&(s+=y),(30==c||C&&c<30)&&(s+=_),(256==c||C&&c<256)&&(s+=k),(261==c||C&&c<261)&&(s+=v),(s=IntToStr(s.length)+s).length>d&&(s=s.substring(0,d)),m.SendDataToHost(e,!0,s,1&n));case 74:l("SCSI: GET_EVENT_STATUS_NOTIFICATION",e,r.charCodeAt(1),r.charCodeAt(4),r.charCodeAt(9)),1!=r.charCodeAt(1)&&16!=r.charCodeAt(4)?(l("SCSI ERROR"),m.SendCommandEndResponse(1,5,e,38,1)):(C=0,(160==e&&null!=m.floppy||176==e&&null!=m.cdrom)&&(C=2),m.SendDataToHost(e,!0,String.fromCharCode(0,C,128,0),1&n));break;case 76:m.SendCommand(81,IntToStrX(0)+IntToStrX(0)+IntToStrX(0)+String.fromCharCode(135,80,3,0,0,0,176,81,5,32,0),!0);break;case 81:return l("SCSI READ_DISC_INFO",e),m.SendCommandEndResponse(0,5,e,32,0);case 85:return l("SCSI ERROR: MODE_SELECT_10",e),m.SendCommandEndResponse(1,5,e,32,0);case 90:l("SCSI: MODE_SENSE_10",e,63&r.charCodeAt(2));var d=ReadShort(r,7),s=null;if(0==d)return m.SendDataToHost(e,!0,IntToStr(60)+IntToStr(8),1&n);var i=0;switch(160==e?null!=m.floppy&&(i=m.floppy.size>>9):null!=m.cdrom&&(i=m.cdrom.size>>11),63&r.charCodeAt(2)){case 1:s=160==e?i<=2880?O:X:w;break;case 5:160==e&&(s=i<=2880?h:f);break;case 63:s=160==e?i<=2880?p:u:g;break;case 26:176==e&&(s=R);break;case 29:176==e&&(s=E);break;case 42:176==e&&(s=I)}null==s?m.SendCommandEndResponse(0,5,e,32,0):m.SendDataToHost(e,!0,s,1&n);break;default:return l("IDER: Unknown SCSI command",r.charCodeAt(0)),m.SendCommandEndResponse(0,5,e,32,0)}}(e,a,o,n),28);case 83:var t;return m.acc.length<14?0:(t=ReadShortX(m.acc,9),m.acc.length<14+t?0:(l("SCSI_WRITE, len = "+(14+t)),m.SendCommand(81,String.fromCharCode(0,0,0,0,0,0,0,0,0,0,0,0,135,112,3,0,0,0,160,81,7,39,0),!0),14+t));default:l("Unknown IDER command",m.acc[0]),m.Stop()}return 0}),s=null;return m} \ No newline at end of file +var CreateAmtRemoteIder=function(){var m={};function l(){urlvars&&urlvars.idertrace&&console.log(...arguments)}m.protocol=3,m.bytesToAmt=0,m.bytesFromAmt=0,m.rx_timeout=3e4,m.tx_timeout=0,m.heartbeat=2e4,m.version=1,m.acc="",m.inSequence=0,m.outSequence=0,m.iderinfo=null,m.enabled=!1,m.iderStart=0,m.floppy=null,m.cdrom=null,m.floppyReady=!1,m.cdromReady=!1,m.pingTimer=null;var f=String.fromCharCode(0,38,49,128,0,0,0,0,5,30,16,169,8,32,2,0,3,195,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0),u=String.fromCharCode(0,92,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0,3,22,0,160,0,0,0,0,0,18,2,0,0,0,0,0,0,0,160,0,0,0,5,30,16,169,8,32,2,0,3,195,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0,8,10,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,17,36,49),h=String.fromCharCode(0,38,36,128,0,0,0,0,5,30,4,176,2,18,2,0,0,80,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0),p=String.fromCharCode(0,92,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0,3,22,0,160,0,0,0,0,0,18,2,0,0,0,0,0,0,0,160,0,0,0,5,30,4,176,2,18,2,0,0,80,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,2,208,0,0,8,10,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,17,36,49),R=String.fromCharCode(0,18,1,128,0,0,0,0,26,10,0,0,0,0,0,0,0,0,0,0),E=String.fromCharCode(0,18,1,128,0,0,0,0,29,10,0,0,0,0,0,0,0,0,0,0),I=String.fromCharCode(0,32,1,128,0,0,0,0,42,24,0,0,0,0,32,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0),g=String.fromCharCode(0,40,1,128,0,0,0,0,1,6,0,255,0,0,0,0,42,24,0,0,0,0,2,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0),b=(String.fromCharCode(0,0,0,40,0,0,0,8),String.fromCharCode(0,0,3,4,0,8,1,0)),A=String.fromCharCode(0,1,3,4,0,0,0,2),T=String.fromCharCode(0,2,3,4,0,0,0,0),D=String.fromCharCode(0,3,3,4,41,0,0,2),y=String.fromCharCode(0,16,1,8,0,0,8,0,0,1,0,0),_=String.fromCharCode(0,30,3,0),k=String.fromCharCode(1,0,3,0),v=String.fromCharCode(1,5,3,0),O=String.fromCharCode(0,18,36,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0),X=String.fromCharCode(0,18,49,128,0,0,0,0,1,10,0,1,0,0,0,0,2,0,0,0),w=String.fromCharCode(0,14,1,128,0,0,0,0,1,6,0,255,0,0,0,0);function F(e,r,n,a){var o=null,t=0;160==e&&(o=m.floppy,null!=m.floppy)&&(t=m.floppy.size>>9),176==e&&(o=m.cdrom,null!=m.cdrom)&&(t=m.cdrom.size>>11),n<0||tm.iderinfo.readbfr&&(n=m.iderinfo.readbfr);c-=n;S+=n;var o=new FileReader;o.onload=function(){m.SendDataToHost(d,0==c,this.result,1&r),0>8,0,a?180:181,0,2,0,255&o,o>>8,e,88,133,0,3,0,0,0,e,80,0,0,0,0,0,0)+n,r,a):m.SendCommand(84,String.fromCharCode(0,255&n.length,n.length>>8,0,a?180:181,0,2,0,255&o,o>>8,e,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0)+n,r,a)},m.SendGetDataFromHost=function(e,r){m.SendCommand(82,String.fromCharCode(0,255&r,r>>8,0,181,0,0,0,255&r,r>>8,e,88,0,0,0,0,0,0,0,0,0,0,0),!1)},m.SendDisableEnableFeatures=function(e,r){null==r&&(r=""),m.SendCommand(72,String.fromCharCode(e)+r)};var d,S,c,C=!(m.ProcessDataEx=function(){if(!(m.acc.length<8))switch(m.acc.charCodeAt(0)){case 65:return m.acc.length<30?0:(t=m.acc.charCodeAt(29),m.acc.length<30+t?0:(m.iderinfo={},m.iderinfo.major=m.acc.charCodeAt(8),m.iderinfo.minor=m.acc.charCodeAt(9),m.iderinfo.fwmajor=m.acc.charCodeAt(10),m.iderinfo.fwminor=m.acc.charCodeAt(11),m.iderinfo.readbfr=ReadShortX(m.acc,16),m.iderinfo.writebfr=ReadShortX(m.acc,18),m.iderinfo.proto=m.acc.charCodeAt(21),m.iderinfo.iana=ReadIntX(m.acc,25),l(m.iderinfo),0!=m.iderinfo.proto&&(l("Unknown proto",m.iderinfo.proto),m.Stop()),8192{switch(r.charCodeAt(0)){case 0:switch(l("SCSI: TEST_UNIT_READY",e),e){case 160:if(null==m.floppy)return m.SendCommandEndResponse(1,2,e,58,0);if(0==m.floppyReady)return m.floppyReady=!0,m.SendCommandEndResponse(1,6,e,40,0);break;case 176:if(null==m.cdrom)return m.SendCommandEndResponse(1,2,e,58,0);if(0==m.cdromReady)return m.cdromReady=!0,m.SendCommandEndResponse(1,6,e,40,0);break;default:return l("SCSI Internal error 3",e)}m.SendCommandEndResponse(1,0,e,0,0);break;case 8:c=((31&r.charCodeAt(1))<<16)+(r.charCodeAt(2)<<8)+r.charCodeAt(3),S=r.charCodeAt(4),l("SCSI: READ_6",e,c,S=0==S?256:S),F(e,c,S,n);break;case 10:return c=((31&r.charCodeAt(1))<<16)+(r.charCodeAt(2)<<8)+r.charCodeAt(3),S=r.charCodeAt(4),l("SCSI: WRITE_6",e,c,S=0==S?256:S),m.SendCommandEndResponse(1,2,e,58,0);case 26:if(l("SCSI: MODE_SENSE_6",e),63==r.charCodeAt(2)&&0==r.charCodeAt(3)){var o=0,t=0;switch(e){case 160:if(null==m.floppy)return m.SendCommandEndResponse(1,2,e,58,0);o=0,t=128;break;case 176:if(null==m.cdrom)return m.SendCommandEndResponse(1,2,e,58,0);o=5,t=128;break;default:return l("SCSI Internal error 6",e)}return m.SendDataToHost(e,!0,String.fromCharCode(0,o,t,0),1&n)}m.SendCommandEndResponse(1,5,e,36,0);break;case 27:m.SendCommandEndResponse(1,0,e);break;case 30:if(l("SCSI: ALLOW_MEDIUM_REMOVAL",e),160==e&&null==m.floppy)return m.SendCommandEndResponse(1,2,e,58,0);if(176==e&&null==m.cdrom)return m.SendCommandEndResponse(1,2,e,58,0);m.SendCommandEndResponse(1,0,e,0,0);break;case 35:l("SCSI: READ_FORMAT_CAPACITIES",e);var d=ReadShort(r,7);switch(e){case 160:if(null==m.floppy||0==m.floppy.size)return m.SendCommandEndResponse(0,5,e,36,0);m.floppy.size;break;case 176:if(null==m.cdrom||0==m.cdrom.size)return m.SendCommandEndResponse(0,5,e,36,0);m.cdrom.size;break;default:return l("SCSI Internal error 4",e)}m.SendDataToHost(e,!0,IntToStr(8)+String.fromCharCode(0,0,11,64,2,0,2,0),1&n);break;case 37:l("SCSI: READ_CAPACITY",e);var S=0;switch(e){case 160:if(null==m.floppy||0==m.floppy.size)return m.SendCommandEndResponse(0,2,e,58,0);l("DEV_FLOPPY",S=null!=m.floppy?(m.floppy.size>>9)-1:S);break;case 176:if(null==m.floppy||0==m.floppy.size)return m.SendCommandEndResponse(0,2,e,58,0);l("DEV_CDDVD",S=null!=m.cdrom?(m.cdrom.size>>11)-1:S);break;default:return l("SCSI Internal error 4",e)}l("SCSI: READ_CAPACITY2",e,a),m.SendDataToHost(a,!0,IntToStr(S)+String.fromCharCode(0,0,176==e?8:2,0),1&n);break;case 40:c=ReadInt(r,2),S=ReadShort(r,7),l("SCSI: READ_10",e,c,S),F(e,c,S,n);break;case 42:case 46:c=ReadInt(r,2),S=ReadShort(r,7),l("SCSI: WRITE_10",e,c,S),m.SendGetDataFromHost(e,512*S);break;case 67:var d=ReadShort(r,7),c=2&r.charCodeAt(1),C=7&r.charCodeAt(2);switch(0==C&&(C=r.charCodeAt(9)>>6),l("SCSI: READ_TOC, dev="+e+", buflen="+d+", msf="+c+", format="+C),e){case 160:return m.SendCommandEndResponse(1,5,e,32,0);case 176:break;default:return l("SCSI Internal error 9",e)}1==C?m.SendDataToHost(e,!0,String.fromCharCode(0,10,1,1,0,20,1,0,0,0,0,0),1&n):0==C&&(c?m.SendDataToHost(e,!0,String.fromCharCode(0,18,1,1,0,20,1,0,0,0,2,0,0,20,170,0,0,0,52,19),1&n):m.SendDataToHost(e,!0,String.fromCharCode(0,18,1,1,0,20,1,0,0,0,0,0,0,20,170,0,0,0,0,0),1&n));break;case 70:C=2!=r.charCodeAt(1),c=ReadShort(r,2),d=ReadShort(r,7);return l("SCSI: GET_CONFIGURATION",e,C,c,d),0==d?m.SendDataToHost(e,!0,IntToStr(60)+IntToStr(8),1&n):(s=IntToStr(8),0==c&&(s+=b),(1==c||C&&c<1)&&(s+=A),(2==c||C&&c<2)&&(s+=T),(3==c||C&&c<3)&&(s+=D),(16==c||C&&c<16)&&(s+=y),(30==c||C&&c<30)&&(s+=_),(256==c||C&&c<256)&&(s+=k),(261==c||C&&c<261)&&(s+=v),(s=IntToStr(s.length)+s).length>d&&(s=s.substring(0,d)),m.SendDataToHost(e,!0,s,1&n));case 74:l("SCSI: GET_EVENT_STATUS_NOTIFICATION",e,r.charCodeAt(1),r.charCodeAt(4),r.charCodeAt(9)),1!=r.charCodeAt(1)&&16!=r.charCodeAt(4)?(l("SCSI ERROR"),m.SendCommandEndResponse(1,5,e,38,1)):(C=0,(160==e&&null!=m.floppy||176==e&&null!=m.cdrom)&&(C=2),m.SendDataToHost(e,!0,String.fromCharCode(0,C,128,0),1&n));break;case 76:m.SendCommand(81,IntToStrX(0)+IntToStrX(0)+IntToStrX(0)+String.fromCharCode(135,80,3,0,0,0,176,81,5,32,0),!0);break;case 81:return l("SCSI READ_DISC_INFO",e),m.SendCommandEndResponse(0,5,e,32,0);case 85:return l("SCSI ERROR: MODE_SELECT_10",e),m.SendCommandEndResponse(1,5,e,32,0);case 90:l("SCSI: MODE_SENSE_10",e,63&r.charCodeAt(2));var d=ReadShort(r,7),s=null;if(0==d)return m.SendDataToHost(e,!0,IntToStr(60)+IntToStr(8),1&n);var i=0;switch(160==e?null!=m.floppy&&(i=m.floppy.size>>9):null!=m.cdrom&&(i=m.cdrom.size>>11),63&r.charCodeAt(2)){case 1:s=160==e?i<=2880?O:X:w;break;case 5:160==e&&(s=i<=2880?h:f);break;case 63:s=160==e?i<=2880?p:u:g;break;case 26:176==e&&(s=R);break;case 29:176==e&&(s=E);break;case 42:176==e&&(s=I)}null==s?m.SendCommandEndResponse(0,5,e,32,0):m.SendDataToHost(e,!0,s,1&n);break;default:return l("IDER: Unknown SCSI command",r.charCodeAt(0)),m.SendCommandEndResponse(0,5,e,32,0)}})(e,a,o,n),28);case 83:var t;return m.acc.length<14?0:(t=ReadShortX(m.acc,9),m.acc.length<14+t?0:(l("SCSI_WRITE, len = "+(14+t)),m.SendCommand(81,String.fromCharCode(0,0,0,0,0,0,0,0,0,0,0,0,135,112,3,0,0,0,160,81,7,39,0),!0),14+t));default:l("Unknown IDER command",m.acc[0]),m.Stop()}return 0}),s=null;return m} \ No newline at end of file diff --git a/public/scripts/amt-redir-ws-0.1.0-min.js b/public/scripts/amt-redir-ws-0.1.0-min.js index f13b0515..dec65dbf 100644 --- a/public/scripts/amt-redir-ws-0.1.0-min.js +++ b/public/scripts/amt-redir-ws-0.1.0-min.js @@ -1 +1 @@ -var CreateAmtRedirect=function(e,o){var y={};function x(e){return String.fromCharCode.apply(null,e)}return((y.m=e).parent=y).authCookie=o,y.State=0,y.socket=null,y.host=null,y.port=0,y.user=null,y.pass=null,y.authuri="/RedirectionService",y.tlsv1only=0,y.inDataCount=0,y.connectstate=0,y.protocol=e.protocol,y.acc=null,y.amtsequence=1,y.amtkeepalivetimer=null,y.onStateChanged=null,y.Start=function(e,t,n,r,a){y.host=e,y.port=t,y.user=n,y.pass=r,y.connectstate=0,y.inDataCount=0;e=window.location.protocol.replace("http","ws")+"//"+window.location.host+window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/webrelay.ashx?p=2&host="+e+"&port="+t+"&tls="+a+("*"==n?"&serverauth=1":"")+(void 0===r?"&serverauth=1&user="+n:"");null!=o&&""!=o&&(e+="&auth="+o),y.socket=new WebSocket(e),y.socket.binaryType="arraybuffer",y.socket.onopen=y.xxOnSocketConnected,y.socket.onmessage=y.xxOnMessage,y.socket.onclose=y.xxOnSocketClosed,y.xxStateChange(1)},y.xxOnSocketConnected=function(){y.xxStateChange(2),1==y.protocol&&y.directSend(new Uint8Array([16,0,0,0,83,79,76,32])),2==y.protocol&&y.directSend(new Uint8Array([16,1,0,0,75,86,77,82])),3==y.protocol&&y.directSend(new Uint8Array([16,0,0,0,73,68,69,82]))},y.xxOnMessage=function(e){if(e.data&&-1!=y.connectstate){if(y.inDataCount++,1==y.connectstate&&(2==y.protocol||3==y.protocol))return y.m.ProcessBinaryData?y.m.ProcessBinaryData(e.data):y.m.ProcessData(x(e.data));var t;for(null==y.acc?y.acc=e.data:((t=new Uint8Array(y.acc.byteLength+e.data.byteLength)).set(new Uint8Array(y.acc),0),t.set(new Uint8Array(e.data),y.acc.byteLength),y.acc=t.buffer);null!=y.acc&&1<=y.acc.byteLength;){var n=0,r=new Uint8Array(y.acc);switch(r[0]){case 17:if(r.byteLength<4)return;var a=r[1];if(0===a){if(r.byteLength<13)return;a=r[12];if(r.byteLength<13+a)return;y.directSend(new Uint8Array([19,0,0,0,0,0,0,0,0])),n=13+a}else y.Stop(1);break;case 20:if(r.byteLength<9)return;var o=new DataView(y.acc).getUint32(5,!0);if(r.byteLength<9+o)return;var a=r[1],c=r[4],s=[];for(i=0;i{for(var t="",n=0;nAmtSetupBinSetupGuids.length)return null;for(var r,t=[],o=AmtSetupBinSetupGuids[e.fileType-1],a=0,o=(o=(o=(o=(o=(o=(o=(o+=ShortToStrX(e.recordChunkCount))+ShortToStrX(e.recordHeaderByteCount))+IntToStrX(e.recordNumber))+String.fromCharCode(e.majorVersion,e.minorVersion))+ShortToStrX(e.flags))+IntToStrX(e.records.length))+IntToStrX(e.dataRecordsConsumed))+ShortToStrX(e.dataRecordChunkCount);o.length<512;)o+="\0";for(r in t.push(o),e.records){var n,i="",d=e.records[r],i=(i=(i=(i=(i=(i=(i+=IntToStrX(d.typeIdentifier))+IntToStrX(d.flags))+IntToStrX(0))+IntToStrX(0))+ShortToStrX(1))+ShortToStrX(24))+IntToStrX(++a);for(n in d.variables.sort(AmtSetupBinVariableCompare),d.variables){var u="",s=d.variables[n],l=s.value;for(s.type=AmtSetupBinVarIds[s.moduleid][s.varid][0],0r.moduleid?1:e.moduleidr.varid?1:e.varidAmtSetupBinSetupGuids.length)return null;for(var r,t=[],o=AmtSetupBinSetupGuids[e.fileType-1],a=0,o=(o=(o=(o=(o=(o=(o=(o+=ShortToStrX(e.recordChunkCount))+ShortToStrX(e.recordHeaderByteCount))+IntToStrX(e.recordNumber))+String.fromCharCode(e.majorVersion,e.minorVersion))+ShortToStrX(e.flags))+IntToStrX(e.records.length))+IntToStrX(e.dataRecordsConsumed))+ShortToStrX(e.dataRecordChunkCount);o.length<512;)o+="\0";for(r in t.push(o),e.records){var n,i="",d=e.records[r],i=(i=(i=(i=(i=(i=(i+=IntToStrX(d.typeIdentifier))+IntToStrX(d.flags))+IntToStrX(0))+IntToStrX(0))+ShortToStrX(1))+ShortToStrX(24))+IntToStrX(++a);for(n in d.variables.sort(AmtSetupBinVariableCompare),d.variables){var u="",s=d.variables[n],l=s.value;for(s.type=AmtSetupBinVarIds[s.moduleid][s.varid][0],0r.moduleid?1:e.moduleid":D=!1,d=0;break;case"7":c=S,s=T,d=0;break;case"8":S=c,T=s,d=0;break;case"M":for(var h=l[1];h>=l[0]+1;h--)for(var a=0;al[0]-1;h--)for(a=0;am.height)&&(T=m.height);break;case"C":1==t&&(0==r[0]?S++:S+=r[0],S>m.width)&&(S=m.width);break;case"D":1==t&&(0==r[0]?S--:S-=r[0],S<0)&&(S=0);break;case"d":1==t&&(T=(T=r[0]-1)>m.height?m.height:T)<0&&(T=0);break;case"G":1==t&&(S=(S=r[0]-1)<0?0:S)>m.width-1&&(S=m.width-1);break;case"P":var a=1;for(1==t&&(a=r[0]),n=S;nm.height&&(r[0]=m.height),r[1]>m.width&&(r[1]=m.width),T=r[0]-1,r[1]-1):T=0;break;case"m":for(n=0;nm.height-1&&(l[0]=m.height-1),l[1]<0&&(l[1]=0),l[1]>m.height-1&&(l[1]=m.height-1),l[0]>l[1]&&(l[0]=l[1]);break;case"S":a=1;1==t&&(a=r[0]);for(var c=l[0];c<=l[1]-a;c++)for(var f=0;fl[0]+a;c--)for(f=0;fl[0];c--)for(f=0;f=m.width&&(s=0,d++);break;default:console.log("Unknown terminal code",e,r,i)}}}(i,b,k+1,v),d=0);break;case 4:case 5:d=0;break;case 6:var o=i.charCodeAt(0);";"==i?k++:7==o?(function(e){var r;0!=e.length&&(0==(r=parseInt(e[0]))||2==r)&&1m.width&&(S=m.width),T>m.height-1&&(T=m.height-1),e){case"\b":0l[1]&&(m.recordLineTobackBuffer(0),x(1),T=l[1]),m.lineFeed="\r",S=0;break;case"\r":S=0;break;default:S>=m.width&&(S=0,C&&T++,T>=m.height-1)&&(x(1),T=m.height-1),o(e),S++}}}function o(e){y[T][S]=e,p[T][S]=(u<<6)+(w<<12)+g}function L(){for(var e=(u<<6)+(w<<12)+g,r=S;r")},m.TermDrawLine=function(e,r,t){for(var i,n,h,a,o=1,c=0;c>a&63],1&i&&(e+=";text-decoration:underline"),e+=';">',t=""+(t=""),o=i),n=y[r][c]){case"&":e+="&";break;case"<":e+="<";break;case">":e+=">";break;case" ":e+=" ";break;default:e+=n}return[e,t]},m.TermDraw=function(){for(var e="",r="",t=0;t")}var n=(B=800"+n+r+e+"",m.DivElement.scrollTop=m.DivElement.scrollHeight,0==m.heightLock&&setTimeout(m.TermLockHeight,10)},m.TermLockHeight=function(){m.heightLock=m.DivElement.clientHeight,m.DivElement.style.height=m.DivElement.parentNode.style.height=m.heightLock+"px",m.DivElement.style["overflow-y"]="scroll"},m.TermInit=function(){m.TermResetScreen()},m.heightLock=0,m.DivElement.style.height="",null!=r&&null!=r.cols&&null!=r.rows?m.Init(r.cols,r.rows):m.Init(),m} \ No newline at end of file +var CreateAmtRemoteTerminal=function(e,r){var l,m={},f=(m.DivId=e,m.DivElement=document.getElementById(e),m.protocol=1,r.protocol&&(m.protocol=r.protocol),m.terminalEmulation=1,m.fxEmulation=0,m.lineFeed="\r\n",m.debugmode=0,m.width=80,m.height=25,m.heightLock=0,["000000","BB0000","00BB00","BBBB00","0000BB","BB00BB","00BBBB","BBBBBB","555555","FF5555","55FF55","FFFF55","5555FF","FF55FF","55FFFF","FFFFFF"]),g=0,w=7,u=0,C=!0,S=0,T=0,c=0,s=0,d=0,b=[],k=0,v=0,p=[],y=[],n=!1,K=!0,D=!1,B=[],F="";m.title=null,m.onTitleChange=null,m.Start=function(){},m.Init=function(e,r){m.width=e||80,m.height=r||25;for(var t=0;t":D=!1,d=0;break;case"7":c=S,s=T,d=0;break;case"8":S=c,T=s,d=0;break;case"M":for(var h=l[1];h>=l[0]+1;h--)for(var a=0;al[0]-1;h--)for(a=0;a{if(1==i)switch(e){case"l":25==r[0]&&(K=!1);break;case"h":25==r[0]&&(K=!0)}else if(0==i){var n,h;switch(e){case"c":m.TermResetScreen();break;case"A":1==t&&(0==r[0]?T--:T-=r[0],T<0)&&(T=0);break;case"B":1==t&&(0==r[0]?T++:T+=r[0],T>m.height)&&(T=m.height);break;case"C":1==t&&(0==r[0]?S++:S+=r[0],S>m.width)&&(S=m.width);break;case"D":1==t&&(0==r[0]?S--:S-=r[0],S<0)&&(S=0);break;case"d":1==t&&(T=(T=r[0]-1)>m.height?m.height:T)<0&&(T=0);break;case"G":1==t&&(S=(S=r[0]-1)<0?0:S)>m.width-1&&(S=m.width-1);break;case"P":var a=1;for(1==t&&(a=r[0]),n=S;nm.height&&(r[0]=m.height),r[1]>m.width&&(r[1]=m.width),T=r[0]-1,r[1]-1):T=0;break;case"m":for(n=0;n{for(var e=(w<<6)+(u<<12)+g,r=0;rm.height-1&&(l[0]=m.height-1),l[1]<0&&(l[1]=0),l[1]>m.height-1&&(l[1]=m.height-1),l[0]>l[1]&&(l[0]=l[1]);break;case"S":a=1;1==t&&(a=r[0]);for(var c=l[0];c<=l[1]-a;c++)for(var f=0;fl[0]+a;c--)for(f=0;fl[0];c--)for(f=0;f=m.width&&(s=0,d++);break;default:console.log("Unknown terminal code",e,r,i)}}})(i,b,k+1,v),d=0);break;case 4:case 5:d=0;break;case 6:var o=i.charCodeAt(0);";"==i?k++:7==o?((e=>{var r;0!=e.length&&(0==(r=parseInt(e[0]))||2==r)&&1m.width&&(S=m.width),T>m.height-1&&(T=m.height-1),e){case"\b":0l[1]&&(m.recordLineTobackBuffer(0),x(1),T=l[1]),m.lineFeed="\r",S=0;break;case"\r":S=0;break;default:S>=m.width&&(S=0,C&&T++,T>=m.height-1)&&(x(1),T=m.height-1),o(e),S++}}}function o(e){y[T][S]=e,p[T][S]=(w<<6)+(u<<12)+g}function L(){for(var e=(w<<6)+(u<<12)+g,r=S;r")},m.TermDrawLine=function(e,r,t){for(var i,n,h,a,o=1,c=0;c>a&63],1&i&&(e+=";text-decoration:underline"),e+=';">',t=""+(t=""),o=i),n=y[r][c]){case"&":e+="&";break;case"<":e+="<";break;case">":e+=">";break;case" ":e+=" ";break;default:e+=n}return[e,t]},m.TermDraw=function(){for(var e="",r="",t=0;t")}var n=(B=800"+n+r+e+"",m.DivElement.scrollTop=m.DivElement.scrollHeight,0==m.heightLock&&setTimeout(m.TermLockHeight,10)},m.TermLockHeight=function(){m.heightLock=m.DivElement.clientHeight,m.DivElement.style.height=m.DivElement.parentNode.style.height=m.heightLock+"px",m.DivElement.style["overflow-y"]="scroll"},m.TermInit=function(){m.TermResetScreen()},m.heightLock=0,m.DivElement.style.height="",null!=r&&null!=r.cols&&null!=r.rows?m.Init(r.cols,r.rows):m.Init(),m} \ No newline at end of file diff --git a/public/scripts/amt-wsman-0.2.0-min.js b/public/scripts/amt-wsman-0.2.0-min.js index 286f8274..fe0e611e 100644 --- a/public/scripts/amt-wsman-0.2.0-min.js +++ b/public/scripts/amt-wsman-0.2.0-min.js @@ -1 +1 @@ -var WsmanStackCreateService=function(e,s,r,a,o,t){var u={};function l(e){if(!e)return"";var s,r=" ";for(s in e)e.hasOwnProperty(s)&&0===s.indexOf("@")&&(r+=s.substring(1)+'="'+e[s]+'" ');return r}function p(e){if(!e)return"";if("string"==typeof e)return e;if(e.InstanceID)return''+e.InstanceID+"";var s,r="";for(s in e)if(e.hasOwnProperty(s)){if(r+='',e[s].ReferenceParameters){var r=(r+="")+(""+e[s].Address+""+e[s].ReferenceParameters.ResourceURI+""),a=e[s].ReferenceParameters.SelectorSet.Selector;if(Array.isArray(a))for(var o=0;o"+a[o].Value+"";else r+=""+a.Value+"";r+=""}else r+=e[s];r+=""}return r+=""}return u.NextMessageId=1,u.Address="/wsman",u.comm=CreateWsmanComm(e,s,r,a,o,t),u.PerformAjax=function(e,a,s,r,o){u.comm.PerformAjax('
"+e,function(e,s,r){200!=s?a(u,null,{Header:{HttpError:s}},s,r):(e=u.ParseWsman(e))&&null!=e?a(u,e.Header.ResourceURI,e,200,r):a(u,null,{Header:{HttpError:s}},601,r)},s,r)},u.CancelAllQueries=function(e){u.comm.CancelAllQueries(e)},u.GetNameFromUrl=function(e){var s=e.lastIndexOf("/");return-1==s?e:e.substring(s+1)},u.ExecSubscribe=function(e,s,r,a,o,t,n,l,d,c){var m="",i="",d=(null!=d&&null!=c&&(m="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken"+d+''+c+"",i=''),l=null!=l?""+l+"":"","http://schemas.xmlsoap.org/ws/2004/08/eventing/Subscribe"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"+p(n)+m+'
'+r+""+i+"PT0.000000S");u.PerformAjax(d+"
",a,o,t,'xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust" xmlns:se="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:m="http://x.com"')},u.ExecUnSubscribe=function(e,s,r,a,o){e="http://schemas.xmlsoap.org/ws/2004/08/eventing/Unsubscribe"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"+p(o)+"";u.PerformAjax(e+"",s,r,a,'xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing"')},u.ExecPut=function(e,s,r,a,o,t){t="http://schemas.xmlsoap.org/ws/2004/09/transfer/Put"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60.000S"+p(t)+""+function(e,s){if(!e||null==s)return"";var r,a=u.GetNameFromUrl(e),o="';for(r in s)if(s.hasOwnProperty(r)&&0!==r.indexOf("__")&&0!==r.indexOf("@")&&void 0!==s[r]&&null!==s[r]&&"function"!=typeof s[r])if("object"==typeof s[r]&&s[r].ReferenceParameters){o+=""+s[r].Address+""+s[r].ReferenceParameters.ResourceURI+"";var t=s[r].ReferenceParameters.SelectorSet.Selector;if(Array.isArray(t))for(var n=0;n"+t[n].Value+"";else o+=""+t.Value+"";o+=""}else if(Array.isArray(s[r]))for(n=0;n"+s[r][n].toString()+"";else o+=""+s[r].toString()+"";return o+=""}(e,s);u.PerformAjax(t+"",r,a,o)},u.ExecCreate=function(e,s,r,a,o,t){var n,l=u.GetNameFromUrl(e),d="http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(t)+"';for(n in s)d+=""+s[n]+"";u.PerformAjax(d+"",r,a,o)},u.ExecCreateXml=function(e,s,r,a,o){var t=u.GetNameFromUrl(e);u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60.000S'+s+"",r,a,o)},u.ExecDelete=function(e,s,r,a,o){e="http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(s)+"";u.PerformAjax(e,r,a,o)},u.ExecGet=function(e,s,r,a){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/transfer/Get"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S",s,r,a)},u.ExecMethod=function(e,s,r,a,o,t,n){var l,d="";for(l in r)if(null!=r[l])if(Array.isArray(r[l]))for(var c in r[l])d+=""+r[l][c]+"";else d+=""+r[l]+"";u.ExecMethodXml(e,s,d,a,o,t,n)},u.ExecMethodXml=function(e,s,r,a,o,t,n){u.PerformAjax(e+"/"+s+""+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(n)+"'+r+"",a,o,t)},u.ExecEnum=function(e,s,r,a){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate"+u.Address+""+e+""+u.NextMessageId+++'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S',s,r,a)},u.ExecPull=function(e,s,r,a,o){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull"+u.Address+""+e+""+u.NextMessageId+++'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S'+s+"99999999",r,a,o)},u.ParseWsman=function(s){try{s.childNodes||(s=function(e){{var s;return window.DOMParser?(new DOMParser).parseFromString(e,"text/xml"):((s=new ActiveXObject("Microsoft.XMLDOM")).async=!1,s.loadXML(e),s)}}(s));var e={Header:{}},r=s.getElementsByTagName("Header")[0];if(!(r=r||s.getElementsByTagName("a:Header")[0]))return null;for(var a=0;a'+e.InstanceID+"";var s,r="";for(s in e)if(e.hasOwnProperty(s)){if(r+='',e[s].ReferenceParameters){var r=(r+="")+(""+e[s].Address+""+e[s].ReferenceParameters.ResourceURI+""),a=e[s].ReferenceParameters.SelectorSet.Selector;if(Array.isArray(a))for(var o=0;o"+a[o].Value+"";else r+=""+a.Value+"";r+=""}else r+=e[s];r+=""}return r+=""}return u.NextMessageId=1,u.Address="/wsman",u.comm=CreateWsmanComm(e,s,r,a,o,t),u.PerformAjax=function(e,a,s,r,o){u.comm.PerformAjax('
"+e,function(e,s,r){200!=s?a(u,null,{Header:{HttpError:s}},s,r):(e=u.ParseWsman(e))&&null!=e?a(u,e.Header.ResourceURI,e,200,r):a(u,null,{Header:{HttpError:s}},601,r)},s,r)},u.CancelAllQueries=function(e){u.comm.CancelAllQueries(e)},u.GetNameFromUrl=function(e){var s=e.lastIndexOf("/");return-1==s?e:e.substring(s+1)},u.ExecSubscribe=function(e,s,r,a,o,t,n,l,c,d){var m="",i="",c=(null!=c&&null!=d&&(m="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken"+c+''+d+"",i=''),l=null!=l?""+l+"":"","http://schemas.xmlsoap.org/ws/2004/08/eventing/Subscribe"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"+p(n)+m+'
'+r+""+i+"PT0.000000S");u.PerformAjax(c+"
",a,o,t,'xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust" xmlns:se="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:m="http://x.com"')},u.ExecUnSubscribe=function(e,s,r,a,o){e="http://schemas.xmlsoap.org/ws/2004/08/eventing/Unsubscribe"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"+p(o)+"";u.PerformAjax(e+"",s,r,a,'xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing"')},u.ExecPut=function(e,s,r,a,o,t){t="http://schemas.xmlsoap.org/ws/2004/09/transfer/Put"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60.000S"+p(t)+""+((e,s)=>{if(!e||null==s)return"";var r,a=u.GetNameFromUrl(e),o="';for(r in s)if(s.hasOwnProperty(r)&&0!==r.indexOf("__")&&0!==r.indexOf("@")&&null!=s[r]&&"function"!=typeof s[r])if("object"==typeof s[r]&&s[r].ReferenceParameters){o+=""+s[r].Address+""+s[r].ReferenceParameters.ResourceURI+"";var t=s[r].ReferenceParameters.SelectorSet.Selector;if(Array.isArray(t))for(var n=0;n"+t[n].Value+"";else o+=""+t.Value+"";o+=""}else if(Array.isArray(s[r]))for(n=0;n"+s[r][n].toString()+"";else o+=""+s[r].toString()+"";return o+=""})(e,s);u.PerformAjax(t+"",r,a,o)},u.ExecCreate=function(e,s,r,a,o,t){var n,l=u.GetNameFromUrl(e),c="http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(t)+"';for(n in s)c+=""+s[n]+"";u.PerformAjax(c+"",r,a,o)},u.ExecCreateXml=function(e,s,r,a,o){var t=u.GetNameFromUrl(e);u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60.000S'+s+"",r,a,o)},u.ExecDelete=function(e,s,r,a,o){e="http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(s)+"";u.PerformAjax(e,r,a,o)},u.ExecGet=function(e,s,r,a){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/transfer/Get"+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S",s,r,a)},u.ExecMethod=function(e,s,r,a,o,t,n){var l,c="";for(l in r)if(null!=r[l])if(Array.isArray(r[l]))for(var d in r[l])c+=""+r[l][d]+"";else c+=""+r[l]+"";u.ExecMethodXml(e,s,c,a,o,t,n)},u.ExecMethodXml=function(e,s,r,a,o,t,n){u.PerformAjax(e+"/"+s+""+u.Address+""+e+""+u.NextMessageId+++"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S"+p(n)+"'+r+"",a,o,t)},u.ExecEnum=function(e,s,r,a){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate"+u.Address+""+e+""+u.NextMessageId+++'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S',s,r,a)},u.ExecPull=function(e,s,r,a,o){u.PerformAjax("http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull"+u.Address+""+e+""+u.NextMessageId+++'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousPT60S'+s+"99999999",r,a,o)},u.ParseWsman=function(s){try{s.childNodes||(s=(e=>{var s;return window.DOMParser?(new DOMParser).parseFromString(e,"text/xml"):((s=new ActiveXObject("Microsoft.XMLDOM")).async=!1,s.loadXML(e),s)})(s));var e={Header:{}},r=s.getElementsByTagName("Header")[0];if(!(r=r||s.getElementsByTagName("a:Header")[0]))return null;for(var a=0;at.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/public/scripts/bootstrap.js b/public/scripts/bootstrap.js new file mode 100644 index 00000000..7afb8336 --- /dev/null +++ b/public/scripts/bootstrap.js @@ -0,0 +1,6314 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bootstrap = factory()); +})(this, (function () { 'use strict'; + + /** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + /** + * Constants + */ + + const elementMap = new Map(); + const Data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); + return; + } + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; + } + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; + } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); + + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); + } + } + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + const MAX_UID = 1000000; + const MILLISECONDS_MULTIPLIER = 1000; + const TRANSITION_END = 'transitionend'; + + /** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ + const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; + }; + + // Shout-out Angus Croll (https://goo.gl/pxwQGp) + const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); + }; + + /** + * Public Util API + */ + + const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; + }; + const getTransitionDurationFromElement = element => { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; + }; + const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); + }; + const isElement$1 = object => { + if (!object || typeof object !== 'object') { + return false; + } + if (typeof object.jquery !== 'undefined') { + object = object[0]; + } + return typeof object.nodeType !== 'undefined'; + }; + const getElement = object => { + // it's a jQuery object or a node element + if (isElement$1(object)) { + return object.jquery ? object[0] : object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; + }; + const isVisible = element => { + if (!isElement$1(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsie appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { + return false; + } + if (summary === null) { + return false; + } + } + return elementIsVisible; + }; + const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; + } + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; + }; + const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { + return null; + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); + }; + const noop = () => {}; + + /** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ + const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions + }; + const getjQuery = () => { + if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { + return window.jQuery; + } + return null; + }; + const DOMContentLoadedCallbacks = []; + const onDOMContentLoaded = callback => { + if (document.readyState === 'loading') { + // add listener on the first call when the document is in loading state + if (!DOMContentLoadedCallbacks.length) { + document.addEventListener('DOMContentLoaded', () => { + for (const callback of DOMContentLoadedCallbacks) { + callback(); + } + }); + } + DOMContentLoadedCallbacks.push(callback); + } else { + callback(); + } + }; + const isRTL = () => document.documentElement.dir === 'rtl'; + const defineJQueryPlugin = plugin => { + onDOMContentLoaded(() => { + const $ = getjQuery(); + /* istanbul ignore if */ + if ($) { + const name = plugin.NAME; + const JQUERY_NO_CONFLICT = $.fn[name]; + $.fn[name] = plugin.jQueryInterface; + $.fn[name].Constructor = plugin; + $.fn[name].noConflict = () => { + $.fn[name] = JQUERY_NO_CONFLICT; + return plugin.jQueryInterface; + }; + } + }); + }; + const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue; + }; + const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { + return; + } + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); + }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); + }; + + /** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ + const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const namespaceRegex = /[^.]*(?=\..*)\.|.*/; + const stripNameRegex = /\..*/; + const stripUidRegex = /::\d+$/; + const eventRegistry = {}; // Events storage + let uidEvent = 1; + const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' + }; + const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); + + /** + * Private methods + */ + + function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; + } + function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; + } + function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + return fn.apply(element, [event]); + }; + } + function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); + } + } + }; + } + function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); + } + function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + // TODO: tooltip passes `false` instead of selector, so we need to check + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + return [isDelegated, callable, typeEvent]; + } + function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); + } + }; + }; + callable = wrapFunction(callable); + } + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; + } + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); + } + function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; + } + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; + } + function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } + } + function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; + } + const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; + const events = getElementEvents(element); + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); + return; + } + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } + } + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const $ = getjQuery(); + const typeEvent = getTypeEvent(event); + const inNamespace = event !== typeEvent; + let jQueryEvent = null; + let bubbles = true; + let nativeDispatch = true; + let defaultPrevented = false; + if (inNamespace && $) { + jQueryEvent = $.Event(event, args); + $(element).trigger(jQueryEvent); + bubbles = !jQueryEvent.isPropagationStopped(); + nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); + defaultPrevented = jQueryEvent.isDefaultPrevented(); + } + const evt = hydrateObj(new Event(event, { + bubbles, + cancelable: true + }), args); + if (defaultPrevented) { + evt.preventDefault(); + } + if (nativeDispatch) { + element.dispatchEvent(evt); + } + if (evt.defaultPrevented && jQueryEvent) { + jQueryEvent.preventDefault(); + } + return evt; + } + }; + function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch (_unused) { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; + } + }); + } + } + return obj; + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + function normalizeData(value) { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch (_unused) { + return value; + } + } + function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); + } + const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; + } + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Class definition + */ + + class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement$1(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement$1(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement$1(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); + } + } + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const VERSION = '5.3.3'; + + /** + * Class definition + */ + + class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; + } + this._element = element; + this._config = this._getConfig(config); + Data.set(this._element, this.constructor.DATA_KEY, this); + } + + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; + } + } + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(callback, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; + } + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; + }; + const SelectorEngine = { + find(selector, element = document.documentElement) { + return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [].concat(...element.children).filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; + } + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; + } + next = next.nextElementSibling; + } + return []; + }, + focusableChildren(element) { + const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map(selector => `${selector}:not([tabindex^="-"])`).join(','); + return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$f = 'alert'; + const DATA_KEY$a = 'bs.alert'; + const EVENT_KEY$b = `.${DATA_KEY$a}`; + const EVENT_CLOSE = `close${EVENT_KEY$b}`; + const EVENT_CLOSED = `closed${EVENT_KEY$b}`; + const CLASS_NAME_FADE$5 = 'fade'; + const CLASS_NAME_SHOW$8 = 'show'; + + /** + * Class definition + */ + + class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME$f; + } + + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; + } + this._element.classList.remove(CLASS_NAME_SHOW$8); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + } + + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Alert.getOrCreateInstance(this); + if (typeof config !== 'string') { + return; + } + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { + throw new TypeError(`No method named "${config}"`); + } + data[config](this); + }); + } + } + + /** + * Data API implementation + */ + + enableDismissTrigger(Alert, 'close'); + + /** + * jQuery + */ + + defineJQueryPlugin(Alert); + + /** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$e = 'button'; + const DATA_KEY$9 = 'bs.button'; + const EVENT_KEY$a = `.${DATA_KEY$9}`; + const DATA_API_KEY$6 = '.data-api'; + const CLASS_NAME_ACTIVE$3 = 'active'; + const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="button"]'; + const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`; + + /** + * Class definition + */ + + class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME$e; + } + + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3)); + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Button.getOrCreateInstance(this); + if (config === 'toggle') { + data[config](); + } + }); + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE$5); + const data = Button.getOrCreateInstance(button); + data.toggle(); + }); + + /** + * jQuery + */ + + defineJQueryPlugin(Button); + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$d = 'swipe'; + const EVENT_KEY$9 = '.bs.swipe'; + const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; + const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; + const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; + const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; + const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; + const POINTER_TYPE_TOUCH = 'touch'; + const POINTER_TYPE_PEN = 'pen'; + const CLASS_NAME_POINTER_EVENT = 'pointer-event'; + const SWIPE_THRESHOLD = 40; + const Default$c = { + endCallback: null, + leftCallback: null, + rightCallback: null + }; + const DefaultType$c = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)' + }; + + /** + * Class definition + */ + + class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; + } + this._config = this._getConfig(config); + this._deltaX = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } + + // Getters + static get Default() { + return Default$c; + } + static get DefaultType() { + return DefaultType$c; + } + static get NAME() { + return NAME$d; + } + + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY$9); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + return; + } + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + } + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + } + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + if (absDeltaX <= SWIPE_THRESHOLD) { + return; + } + const direction = absDeltaX / this._deltaX; + this._deltaX = 0; + if (!direction) { + return; + } + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); + } + } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } + + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$c = 'carousel'; + const DATA_KEY$8 = 'bs.carousel'; + const EVENT_KEY$8 = `.${DATA_KEY$8}`; + const DATA_API_KEY$5 = '.data-api'; + const ARROW_LEFT_KEY$1 = 'ArrowLeft'; + const ARROW_RIGHT_KEY$1 = 'ArrowRight'; + const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch + + const ORDER_NEXT = 'next'; + const ORDER_PREV = 'prev'; + const DIRECTION_LEFT = 'left'; + const DIRECTION_RIGHT = 'right'; + const EVENT_SLIDE = `slide${EVENT_KEY$8}`; + const EVENT_SLID = `slid${EVENT_KEY$8}`; + const EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`; + const EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`; + const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`; + const EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`; + const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; + const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; + const CLASS_NAME_CAROUSEL = 'carousel'; + const CLASS_NAME_ACTIVE$2 = 'active'; + const CLASS_NAME_SLIDE = 'slide'; + const CLASS_NAME_END = 'carousel-item-end'; + const CLASS_NAME_START = 'carousel-item-start'; + const CLASS_NAME_NEXT = 'carousel-item-next'; + const CLASS_NAME_PREV = 'carousel-item-prev'; + const SELECTOR_ACTIVE = '.active'; + const SELECTOR_ITEM = '.carousel-item'; + const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; + const SELECTOR_ITEM_IMG = '.carousel-item img'; + const SELECTOR_INDICATORS = '.carousel-indicators'; + const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; + const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'; + const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT + }; + const Default$b = { + interval: 5000, + keyboard: true, + pause: 'hover', + ride: false, + touch: true, + wrap: true + }; + const DefaultType$b = { + interval: '(number|boolean)', + // TODO:v6 remove boolean support + keyboard: 'boolean', + pause: '(string|boolean)', + ride: '(boolean|string)', + touch: 'boolean', + wrap: 'boolean' + }; + + /** + * Class definition + */ + + class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + this._interval = null; + this._activeElement = null; + this._isSliding = false; + this.touchTimeout = null; + this._swipeHelper = null; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._addEventListeners(); + if (this._config.ride === CLASS_NAME_CAROUSEL) { + this.cycle(); + } + } + + // Getters + static get Default() { + return Default$b; + } + static get DefaultType() { + return DefaultType$b; + } + static get NAME() { + return NAME$c; + } + + // Public + next() { + this._slide(ORDER_NEXT); + } + nextWhenVisible() { + // FIXME TODO use `document.visibilityState` + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && isVisible(this._element)) { + this.next(); + } + } + prev() { + this._slide(ORDER_PREV); + } + pause() { + if (this._isSliding) { + triggerTransitionEnd(this._element); + } + this._clearInterval(); + } + cycle() { + this._clearInterval(); + this._updateInterval(); + this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval); + } + _maybeEnableCycle() { + if (!this._config.ride) { + return; + } + if (this._isSliding) { + EventHandler.one(this._element, EVENT_SLID, () => this.cycle()); + return; + } + this.cycle(); + } + to(index) { + const items = this._getItems(); + if (index > items.length - 1 || index < 0) { + return; + } + if (this._isSliding) { + EventHandler.one(this._element, EVENT_SLID, () => this.to(index)); + return; + } + const activeIndex = this._getItemIndex(this._getActive()); + if (activeIndex === index) { + return; + } + const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV; + this._slide(order, items[index]); + } + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.defaultInterval = config.interval; + return config; + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event)); + } + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); + } + if (this._config.touch && Swipe.isSupported()) { + this._addTouchEventListeners(); + } + } + _addTouchEventListeners() { + for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { + EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()); + } + const endCallBack = () => { + if (this._config.pause !== 'hover') { + return; + } + + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + + this.pause(); + if (this.touchTimeout) { + clearTimeout(this.touchTimeout); + } + this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval); + }; + const swipeConfig = { + leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)), + rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)), + endCallback: endCallBack + }; + this._swipeHelper = new Swipe(this._element, swipeConfig); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._slide(this._directionToOrder(direction)); + } + } + _getItemIndex(element) { + return this._getItems().indexOf(element); + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; + } + const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2); + activeIndicator.removeAttribute('aria-current'); + const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActiveIndicator) { + newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2); + newActiveIndicator.setAttribute('aria-current', 'true'); + } + } + _updateInterval() { + const element = this._activeElement || this._getActive(); + if (!element) { + return; + } + const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10); + this._config.interval = elementInterval || this._config.defaultInterval; + } + _slide(order, element = null) { + if (this._isSliding) { + return; + } + const activeElement = this._getActive(); + const isNext = order === ORDER_NEXT; + const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap); + if (nextElement === activeElement) { + return; + } + const nextElementIndex = this._getItemIndex(nextElement); + const triggerEvent = eventName => { + return EventHandler.trigger(this._element, eventName, { + relatedTarget: nextElement, + direction: this._orderToDirection(order), + from: this._getItemIndex(activeElement), + to: nextElementIndex + }); + }; + const slideEvent = triggerEvent(EVENT_SLIDE); + if (slideEvent.defaultPrevented) { + return; + } + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + // TODO: change tests that use empty divs to avoid this check + return; + } + const isCycling = Boolean(this._interval); + this.pause(); + this._isSliding = true; + this._setActiveIndicatorElement(nextElementIndex); + this._activeElement = nextElement; + const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END; + const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV; + nextElement.classList.add(orderClassName); + reflow(nextElement); + activeElement.classList.add(directionalClassName); + nextElement.classList.add(directionalClassName); + const completeCallBack = () => { + nextElement.classList.remove(directionalClassName, orderClassName); + nextElement.classList.add(CLASS_NAME_ACTIVE$2); + activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName); + this._isSliding = false; + triggerEvent(EVENT_SLID); + }; + this._queueCallback(completeCallBack, activeElement, this._isAnimated()); + if (isCycling) { + this.cycle(); + } + } + _isAnimated() { + return this._element.classList.contains(CLASS_NAME_SLIDE); + } + _getActive() { + return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + } + _directionToOrder(direction) { + if (isRTL()) { + return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT; + } + return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV; + } + _orderToDirection(order) { + if (isRTL()) { + return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Carousel.getOrCreateInstance(this, config); + if (typeof config === 'number') { + data.to(config); + return; + } + if (typeof config === 'string') { + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + } + }); + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + carousel._maybeEnableCycle(); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + carousel._maybeEnableCycle(); + return; + } + carousel.prev(); + carousel._maybeEnableCycle(); + }); + EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } + }); + + /** + * jQuery + */ + + defineJQueryPlugin(Carousel); + + /** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$b = 'collapse'; + const DATA_KEY$7 = 'bs.collapse'; + const EVENT_KEY$7 = `.${DATA_KEY$7}`; + const DATA_API_KEY$4 = '.data-api'; + const EVENT_SHOW$6 = `show${EVENT_KEY$7}`; + const EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`; + const EVENT_HIDE$6 = `hide${EVENT_KEY$7}`; + const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`; + const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`; + const CLASS_NAME_SHOW$7 = 'show'; + const CLASS_NAME_COLLAPSE = 'collapse'; + const CLASS_NAME_COLLAPSING = 'collapsing'; + const CLASS_NAME_COLLAPSED = 'collapsed'; + const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; + const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; + const WIDTH = 'width'; + const HEIGHT = 'height'; + const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; + const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="collapse"]'; + const Default$a = { + parent: null, + toggle: true + }; + const DefaultType$a = { + parent: '(null|element)', + toggle: 'boolean' + }; + + /** + * Class definition + */ + + class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); + } + } + + // Getters + static get Default() { + return Default$a; + } + static get DefaultType() { + return DefaultType$a; + } + static get NAME() { + return NAME$b; + } + + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); + } + } + show() { + if (this._isTransitioning || this._isShown()) { + return; + } + let activeChildren = []; + + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); + } + if (activeChildren.length && activeChildren[0]._isTransitioning) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6); + if (startEvent.defaultPrevented) { + return; + } + for (const activeInstance of activeChildren) { + activeInstance.hide(); + } + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN$6); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); + } + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN$6); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW$7); + } + + // Private + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); + } + } + } + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; + } + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); + } + } + + // Static + static jQueryInterface(config) { + const _config = {}; + if (typeof config === 'string' && /show|hide/.test(config)) { + _config.toggle = false; + } + return this.each(function () { + const data = Collapse.getOrCreateInstance(this, _config); + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + } + }); + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } + }); + + /** + * jQuery + */ + + defineJQueryPlugin(Collapse); + + var top = 'top'; + var bottom = 'bottom'; + var right = 'right'; + var left = 'left'; + var auto = 'auto'; + var basePlacements = [top, bottom, right, left]; + var start = 'start'; + var end = 'end'; + var clippingParents = 'clippingParents'; + var viewport = 'viewport'; + var popper = 'popper'; + var reference = 'reference'; + var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) { + return acc.concat([placement + "-" + start, placement + "-" + end]); + }, []); + var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) { + return acc.concat([placement, placement + "-" + start, placement + "-" + end]); + }, []); // modifiers that need to read the DOM + + var beforeRead = 'beforeRead'; + var read = 'read'; + var afterRead = 'afterRead'; // pure-logic modifiers + + var beforeMain = 'beforeMain'; + var main = 'main'; + var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state) + + var beforeWrite = 'beforeWrite'; + var write = 'write'; + var afterWrite = 'afterWrite'; + var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; + + function getNodeName(element) { + return element ? (element.nodeName || '').toLowerCase() : null; + } + + function getWindow(node) { + if (node == null) { + return window; + } + + if (node.toString() !== '[object Window]') { + var ownerDocument = node.ownerDocument; + return ownerDocument ? ownerDocument.defaultView || window : window; + } + + return node; + } + + function isElement(node) { + var OwnElement = getWindow(node).Element; + return node instanceof OwnElement || node instanceof Element; + } + + function isHTMLElement(node) { + var OwnElement = getWindow(node).HTMLElement; + return node instanceof OwnElement || node instanceof HTMLElement; + } + + function isShadowRoot(node) { + // IE 11 has no ShadowRoot + if (typeof ShadowRoot === 'undefined') { + return false; + } + + var OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; + } + + // and applies them to the HTMLElements such as popper and arrow + + function applyStyles(_ref) { + var state = _ref.state; + Object.keys(state.elements).forEach(function (name) { + var style = state.styles[name] || {}; + var attributes = state.attributes[name] || {}; + var element = state.elements[name]; // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } // Flow doesn't support to extend this property, but it's the most + // effective way to apply styles to an HTMLElement + // $FlowFixMe[cannot-write] + + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (name) { + var value = attributes[name]; + + if (value === false) { + element.removeAttribute(name); + } else { + element.setAttribute(name, value === true ? '' : value); + } + }); + }); + } + + function effect$2(_ref2) { + var state = _ref2.state; + var initialStyles = { + popper: { + position: state.options.strategy, + left: '0', + top: '0', + margin: '0' + }, + arrow: { + position: 'absolute' + }, + reference: {} + }; + Object.assign(state.elements.popper.style, initialStyles.popper); + state.styles = initialStyles; + + if (state.elements.arrow) { + Object.assign(state.elements.arrow.style, initialStyles.arrow); + } + + return function () { + Object.keys(state.elements).forEach(function (name) { + var element = state.elements[name]; + var attributes = state.attributes[name] || {}; + var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them + + var style = styleProperties.reduce(function (style, property) { + style[property] = ''; + return style; + }, {}); // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (attribute) { + element.removeAttribute(attribute); + }); + }); + }; + } // eslint-disable-next-line import/no-unused-modules + + + const applyStyles$1 = { + name: 'applyStyles', + enabled: true, + phase: 'write', + fn: applyStyles, + effect: effect$2, + requires: ['computeStyles'] + }; + + function getBasePlacement(placement) { + return placement.split('-')[0]; + } + + var max = Math.max; + var min = Math.min; + var round = Math.round; + + function getUAString() { + var uaData = navigator.userAgentData; + + if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) { + return uaData.brands.map(function (item) { + return item.brand + "/" + item.version; + }).join(' '); + } + + return navigator.userAgent; + } + + function isLayoutViewport() { + return !/^((?!chrome|android).)*safari/i.test(getUAString()); + } + + function getBoundingClientRect(element, includeScale, isFixedStrategy) { + if (includeScale === void 0) { + includeScale = false; + } + + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + + var clientRect = element.getBoundingClientRect(); + var scaleX = 1; + var scaleY = 1; + + if (includeScale && isHTMLElement(element)) { + scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1; + scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1; + } + + var _ref = isElement(element) ? getWindow(element) : window, + visualViewport = _ref.visualViewport; + + var addVisualOffsets = !isLayoutViewport() && isFixedStrategy; + var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX; + var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY; + var width = clientRect.width / scaleX; + var height = clientRect.height / scaleY; + return { + width: width, + height: height, + top: y, + right: x + width, + bottom: y + height, + left: x, + x: x, + y: y + }; + } + + // means it doesn't take into account transforms. + + function getLayoutRect(element) { + var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed. + // Fixes https://github.com/popperjs/popper-core/issues/1223 + + var width = element.offsetWidth; + var height = element.offsetHeight; + + if (Math.abs(clientRect.width - width) <= 1) { + width = clientRect.width; + } + + if (Math.abs(clientRect.height - height) <= 1) { + height = clientRect.height; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: width, + height: height + }; + } + + function contains(parent, child) { + var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method + + if (parent.contains(child)) { + return true; + } // then fallback to custom implementation with Shadow DOM support + else if (rootNode && isShadowRoot(rootNode)) { + var next = child; + + do { + if (next && parent.isSameNode(next)) { + return true; + } // $FlowFixMe[prop-missing]: need a better way to handle this... + + + next = next.parentNode || next.host; + } while (next); + } // Give up, the result is false + + + return false; + } + + function getComputedStyle$1(element) { + return getWindow(element).getComputedStyle(element); + } + + function isTableElement(element) { + return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0; + } + + function getDocumentElement(element) { + // $FlowFixMe[incompatible-return]: assume body is always available + return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing] + element.document) || window.document).documentElement; + } + + function getParentNode(element) { + if (getNodeName(element) === 'html') { + return element; + } + + return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle + // $FlowFixMe[incompatible-return] + // $FlowFixMe[prop-missing] + element.assignedSlot || // step into the shadow DOM of the parent of a slotted node + element.parentNode || ( // DOM Element detected + isShadowRoot(element) ? element.host : null) || // ShadowRoot detected + // $FlowFixMe[incompatible-call]: HTMLElement is a Node + getDocumentElement(element) // fallback + + ); + } + + function getTrueOffsetParent(element) { + if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837 + getComputedStyle$1(element).position === 'fixed') { + return null; + } + + return element.offsetParent; + } // `.offsetParent` reports `null` for fixed elements, while absolute elements + // return the containing block + + + function getContainingBlock(element) { + var isFirefox = /firefox/i.test(getUAString()); + var isIE = /Trident/i.test(getUAString()); + + if (isIE && isHTMLElement(element)) { + // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport + var elementCss = getComputedStyle$1(element); + + if (elementCss.position === 'fixed') { + return null; + } + } + + var currentNode = getParentNode(element); + + if (isShadowRoot(currentNode)) { + currentNode = currentNode.host; + } + + while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) { + var css = getComputedStyle$1(currentNode); // This is non-exhaustive but covers the most common CSS properties that + // create a containing block. + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + + if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') { + return currentNode; + } else { + currentNode = currentNode.parentNode; + } + } + + return null; + } // Gets the closest ancestor positioned element. Handles some edge cases, + // such as table ancestors and cross browser bugs. + + + function getOffsetParent(element) { + var window = getWindow(element); + var offsetParent = getTrueOffsetParent(element); + + while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') { + offsetParent = getTrueOffsetParent(offsetParent); + } + + if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static')) { + return window; + } + + return offsetParent || getContainingBlock(element) || window; + } + + function getMainAxisFromPlacement(placement) { + return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y'; + } + + function within(min$1, value, max$1) { + return max(min$1, min(value, max$1)); + } + function withinMaxClamp(min, value, max) { + var v = within(min, value, max); + return v > max ? max : v; + } + + function getFreshSideObject() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0 + }; + } + + function mergePaddingObject(paddingObject) { + return Object.assign({}, getFreshSideObject(), paddingObject); + } + + function expandToHashMap(value, keys) { + return keys.reduce(function (hashMap, key) { + hashMap[key] = value; + return hashMap; + }, {}); + } + + var toPaddingObject = function toPaddingObject(padding, state) { + padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, { + placement: state.placement + })) : padding; + return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + }; + + function arrow(_ref) { + var _state$modifiersData$; + + var state = _ref.state, + name = _ref.name, + options = _ref.options; + var arrowElement = state.elements.arrow; + var popperOffsets = state.modifiersData.popperOffsets; + var basePlacement = getBasePlacement(state.placement); + var axis = getMainAxisFromPlacement(basePlacement); + var isVertical = [left, right].indexOf(basePlacement) >= 0; + var len = isVertical ? 'height' : 'width'; + + if (!arrowElement || !popperOffsets) { + return; + } + + var paddingObject = toPaddingObject(options.padding, state); + var arrowRect = getLayoutRect(arrowElement); + var minProp = axis === 'y' ? top : left; + var maxProp = axis === 'y' ? bottom : right; + var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len]; + var startDiff = popperOffsets[axis] - state.rects.reference[axis]; + var arrowOffsetParent = getOffsetParent(arrowElement); + var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; + var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is + // outside of the popper bounds + + var min = paddingObject[minProp]; + var max = clientSize - arrowRect[len] - paddingObject[maxProp]; + var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; + var offset = within(min, center, max); // Prevents breaking syntax highlighting... + + var axisProp = axis; + state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$); + } + + function effect$1(_ref2) { + var state = _ref2.state, + options = _ref2.options; + var _options$element = options.element, + arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element; + + if (arrowElement == null) { + return; + } // CSS selector + + + if (typeof arrowElement === 'string') { + arrowElement = state.elements.popper.querySelector(arrowElement); + + if (!arrowElement) { + return; + } + } + + if (!contains(state.elements.popper, arrowElement)) { + return; + } + + state.elements.arrow = arrowElement; + } // eslint-disable-next-line import/no-unused-modules + + + const arrow$1 = { + name: 'arrow', + enabled: true, + phase: 'main', + fn: arrow, + effect: effect$1, + requires: ['popperOffsets'], + requiresIfExists: ['preventOverflow'] + }; + + function getVariation(placement) { + return placement.split('-')[1]; + } + + var unsetSides = { + top: 'auto', + right: 'auto', + bottom: 'auto', + left: 'auto' + }; // Round the offsets to the nearest suitable subpixel based on the DPR. + // Zooming can change the DPR, but it seems to report a value that will + // cleanly divide the values into the appropriate subpixels. + + function roundOffsetsByDPR(_ref, win) { + var x = _ref.x, + y = _ref.y; + var dpr = win.devicePixelRatio || 1; + return { + x: round(x * dpr) / dpr || 0, + y: round(y * dpr) / dpr || 0 + }; + } + + function mapToStyles(_ref2) { + var _Object$assign2; + + var popper = _ref2.popper, + popperRect = _ref2.popperRect, + placement = _ref2.placement, + variation = _ref2.variation, + offsets = _ref2.offsets, + position = _ref2.position, + gpuAcceleration = _ref2.gpuAcceleration, + adaptive = _ref2.adaptive, + roundOffsets = _ref2.roundOffsets, + isFixed = _ref2.isFixed; + var _offsets$x = offsets.x, + x = _offsets$x === void 0 ? 0 : _offsets$x, + _offsets$y = offsets.y, + y = _offsets$y === void 0 ? 0 : _offsets$y; + + var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({ + x: x, + y: y + }) : { + x: x, + y: y + }; + + x = _ref3.x; + y = _ref3.y; + var hasX = offsets.hasOwnProperty('x'); + var hasY = offsets.hasOwnProperty('y'); + var sideX = left; + var sideY = top; + var win = window; + + if (adaptive) { + var offsetParent = getOffsetParent(popper); + var heightProp = 'clientHeight'; + var widthProp = 'clientWidth'; + + if (offsetParent === getWindow(popper)) { + offsetParent = getDocumentElement(popper); + + if (getComputedStyle$1(offsetParent).position !== 'static' && position === 'absolute') { + heightProp = 'scrollHeight'; + widthProp = 'scrollWidth'; + } + } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it + + + offsetParent = offsetParent; + + if (placement === top || (placement === left || placement === right) && variation === end) { + sideY = bottom; + var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing] + offsetParent[heightProp]; + y -= offsetY - popperRect.height; + y *= gpuAcceleration ? 1 : -1; + } + + if (placement === left || (placement === top || placement === bottom) && variation === end) { + sideX = right; + var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing] + offsetParent[widthProp]; + x -= offsetX - popperRect.width; + x *= gpuAcceleration ? 1 : -1; + } + } + + var commonStyles = Object.assign({ + position: position + }, adaptive && unsetSides); + + var _ref4 = roundOffsets === true ? roundOffsetsByDPR({ + x: x, + y: y + }, getWindow(popper)) : { + x: x, + y: y + }; + + x = _ref4.x; + y = _ref4.y; + + if (gpuAcceleration) { + var _Object$assign; + + return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); + } + + return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2)); + } + + function computeStyles(_ref5) { + var state = _ref5.state, + options = _ref5.options; + var _options$gpuAccelerat = options.gpuAcceleration, + gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, + _options$adaptive = options.adaptive, + adaptive = _options$adaptive === void 0 ? true : _options$adaptive, + _options$roundOffsets = options.roundOffsets, + roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; + var commonStyles = { + placement: getBasePlacement(state.placement), + variation: getVariation(state.placement), + popper: state.elements.popper, + popperRect: state.rects.popper, + gpuAcceleration: gpuAcceleration, + isFixed: state.options.strategy === 'fixed' + }; + + if (state.modifiersData.popperOffsets != null) { + state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.popperOffsets, + position: state.options.strategy, + adaptive: adaptive, + roundOffsets: roundOffsets + }))); + } + + if (state.modifiersData.arrow != null) { + state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.arrow, + position: 'absolute', + adaptive: false, + roundOffsets: roundOffsets + }))); + } + + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-placement': state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + const computeStyles$1 = { + name: 'computeStyles', + enabled: true, + phase: 'beforeWrite', + fn: computeStyles, + data: {} + }; + + var passive = { + passive: true + }; + + function effect(_ref) { + var state = _ref.state, + instance = _ref.instance, + options = _ref.options; + var _options$scroll = options.scroll, + scroll = _options$scroll === void 0 ? true : _options$scroll, + _options$resize = options.resize, + resize = _options$resize === void 0 ? true : _options$resize; + var window = getWindow(state.elements.popper); + var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); + + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.addEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.addEventListener('resize', instance.update, passive); + } + + return function () { + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.removeEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.removeEventListener('resize', instance.update, passive); + } + }; + } // eslint-disable-next-line import/no-unused-modules + + + const eventListeners = { + name: 'eventListeners', + enabled: true, + phase: 'write', + fn: function fn() {}, + effect: effect, + data: {} + }; + + var hash$1 = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' + }; + function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, function (matched) { + return hash$1[matched]; + }); + } + + var hash = { + start: 'end', + end: 'start' + }; + function getOppositeVariationPlacement(placement) { + return placement.replace(/start|end/g, function (matched) { + return hash[matched]; + }); + } + + function getWindowScroll(node) { + var win = getWindow(node); + var scrollLeft = win.pageXOffset; + var scrollTop = win.pageYOffset; + return { + scrollLeft: scrollLeft, + scrollTop: scrollTop + }; + } + + function getWindowScrollBarX(element) { + // If has a CSS width greater than the viewport, then this will be + // incorrect for RTL. + // Popper 1 is broken in this case and never had a bug report so let's assume + // it's not an issue. I don't think anyone ever specifies width on + // anyway. + // Browsers where the left scrollbar doesn't cause an issue report `0` for + // this (e.g. Edge 2019, IE11, Safari) + return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; + } + + function getViewportRect(element, strategy) { + var win = getWindow(element); + var html = getDocumentElement(element); + var visualViewport = win.visualViewport; + var width = html.clientWidth; + var height = html.clientHeight; + var x = 0; + var y = 0; + + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + var layoutViewport = isLayoutViewport(); + + if (layoutViewport || !layoutViewport && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + + return { + width: width, + height: height, + x: x + getWindowScrollBarX(element), + y: y + }; + } + + // of the `` and `` rect bounds if horizontally scrollable + + function getDocumentRect(element) { + var _element$ownerDocumen; + + var html = getDocumentElement(element); + var winScroll = getWindowScroll(element); + var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; + var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); + var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); + var x = -winScroll.scrollLeft + getWindowScrollBarX(element); + var y = -winScroll.scrollTop; + + if (getComputedStyle$1(body || html).direction === 'rtl') { + x += max(html.clientWidth, body ? body.clientWidth : 0) - width; + } + + return { + width: width, + height: height, + x: x, + y: y + }; + } + + function isScrollParent(element) { + // Firefox wants us to check `-x` and `-y` variations as well + var _getComputedStyle = getComputedStyle$1(element), + overflow = _getComputedStyle.overflow, + overflowX = _getComputedStyle.overflowX, + overflowY = _getComputedStyle.overflowY; + + return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); + } + + function getScrollParent(node) { + if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) { + // $FlowFixMe[incompatible-return]: assume body is always available + return node.ownerDocument.body; + } + + if (isHTMLElement(node) && isScrollParent(node)) { + return node; + } + + return getScrollParent(getParentNode(node)); + } + + /* + given a DOM element, return the list of all scroll parents, up the list of ancesors + until we get to the top window object. This list is what we attach scroll listeners + to, because if any of these parent elements scroll, we'll need to re-calculate the + reference element's position. + */ + + function listScrollParents(element, list) { + var _element$ownerDocumen; + + if (list === void 0) { + list = []; + } + + var scrollParent = getScrollParent(element); + var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); + var win = getWindow(scrollParent); + var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; + var updatedList = list.concat(target); + return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here + updatedList.concat(listScrollParents(getParentNode(target))); + } + + function rectToClientRect(rect) { + return Object.assign({}, rect, { + left: rect.x, + top: rect.y, + right: rect.x + rect.width, + bottom: rect.y + rect.height + }); + } + + function getInnerBoundingClientRect(element, strategy) { + var rect = getBoundingClientRect(element, false, strategy === 'fixed'); + rect.top = rect.top + element.clientTop; + rect.left = rect.left + element.clientLeft; + rect.bottom = rect.top + element.clientHeight; + rect.right = rect.left + element.clientWidth; + rect.width = element.clientWidth; + rect.height = element.clientHeight; + rect.x = rect.left; + rect.y = rect.top; + return rect; + } + + function getClientRectFromMixedType(element, clippingParent, strategy) { + return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element))); + } // A "clipping parent" is an overflowable container with the characteristic of + // clipping (or hiding) overflowing elements with a position different from + // `initial` + + + function getClippingParents(element) { + var clippingParents = listScrollParents(getParentNode(element)); + var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle$1(element).position) >= 0; + var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; + + if (!isElement(clipperElement)) { + return []; + } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414 + + + return clippingParents.filter(function (clippingParent) { + return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body'; + }); + } // Gets the maximum area that the element is visible in due to any number of + // clipping parents + + + function getClippingRect(element, boundary, rootBoundary, strategy) { + var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary); + var clippingParents = [].concat(mainClippingParents, [rootBoundary]); + var firstClippingParent = clippingParents[0]; + var clippingRect = clippingParents.reduce(function (accRect, clippingParent) { + var rect = getClientRectFromMixedType(element, clippingParent, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromMixedType(element, firstClippingParent, strategy)); + clippingRect.width = clippingRect.right - clippingRect.left; + clippingRect.height = clippingRect.bottom - clippingRect.top; + clippingRect.x = clippingRect.left; + clippingRect.y = clippingRect.top; + return clippingRect; + } + + function computeOffsets(_ref) { + var reference = _ref.reference, + element = _ref.element, + placement = _ref.placement; + var basePlacement = placement ? getBasePlacement(placement) : null; + var variation = placement ? getVariation(placement) : null; + var commonX = reference.x + reference.width / 2 - element.width / 2; + var commonY = reference.y + reference.height / 2 - element.height / 2; + var offsets; + + switch (basePlacement) { + case top: + offsets = { + x: commonX, + y: reference.y - element.height + }; + break; + + case bottom: + offsets = { + x: commonX, + y: reference.y + reference.height + }; + break; + + case right: + offsets = { + x: reference.x + reference.width, + y: commonY + }; + break; + + case left: + offsets = { + x: reference.x - element.width, + y: commonY + }; + break; + + default: + offsets = { + x: reference.x, + y: reference.y + }; + } + + var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; + + if (mainAxis != null) { + var len = mainAxis === 'y' ? 'height' : 'width'; + + switch (variation) { + case start: + offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2); + break; + + case end: + offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2); + break; + } + } + + return offsets; + } + + function detectOverflow(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + _options$placement = _options.placement, + placement = _options$placement === void 0 ? state.placement : _options$placement, + _options$strategy = _options.strategy, + strategy = _options$strategy === void 0 ? state.strategy : _options$strategy, + _options$boundary = _options.boundary, + boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, + _options$rootBoundary = _options.rootBoundary, + rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, + _options$elementConte = _options.elementContext, + elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, + _options$altBoundary = _options.altBoundary, + altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, + _options$padding = _options.padding, + padding = _options$padding === void 0 ? 0 : _options$padding; + var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + var altContext = elementContext === popper ? reference : popper; + var popperRect = state.rects.popper; + var element = state.elements[altBoundary ? altContext : elementContext]; + var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy); + var referenceClientRect = getBoundingClientRect(state.elements.reference); + var popperOffsets = computeOffsets({ + reference: referenceClientRect, + element: popperRect, + strategy: 'absolute', + placement: placement + }); + var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets)); + var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect + // 0 or negative = within the clipping rect + + var overflowOffsets = { + top: clippingClientRect.top - elementClientRect.top + paddingObject.top, + bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, + left: clippingClientRect.left - elementClientRect.left + paddingObject.left, + right: elementClientRect.right - clippingClientRect.right + paddingObject.right + }; + var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element + + if (elementContext === popper && offsetData) { + var offset = offsetData[placement]; + Object.keys(overflowOffsets).forEach(function (key) { + var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; + var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x'; + overflowOffsets[key] += offset[axis] * multiply; + }); + } + + return overflowOffsets; + } + + function computeAutoPlacement(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + placement = _options.placement, + boundary = _options.boundary, + rootBoundary = _options.rootBoundary, + padding = _options.padding, + flipVariations = _options.flipVariations, + _options$allowedAutoP = _options.allowedAutoPlacements, + allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; + var variation = getVariation(placement); + var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) { + return getVariation(placement) === variation; + }) : basePlacements; + var allowedPlacements = placements$1.filter(function (placement) { + return allowedAutoPlacements.indexOf(placement) >= 0; + }); + + if (allowedPlacements.length === 0) { + allowedPlacements = placements$1; + } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions... + + + var overflows = allowedPlacements.reduce(function (acc, placement) { + acc[placement] = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding + })[getBasePlacement(placement)]; + return acc; + }, {}); + return Object.keys(overflows).sort(function (a, b) { + return overflows[a] - overflows[b]; + }); + } + + function getExpandedFallbackPlacements(placement) { + if (getBasePlacement(placement) === auto) { + return []; + } + + var oppositePlacement = getOppositePlacement(placement); + return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; + } + + function flip(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + + if (state.modifiersData[name]._skip) { + return; + } + + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, + specifiedFallbackPlacements = options.fallbackPlacements, + padding = options.padding, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + _options$flipVariatio = options.flipVariations, + flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, + allowedAutoPlacements = options.allowedAutoPlacements; + var preferredPlacement = state.options.placement; + var basePlacement = getBasePlacement(preferredPlacement); + var isBasePlacement = basePlacement === preferredPlacement; + var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); + var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) { + return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + flipVariations: flipVariations, + allowedAutoPlacements: allowedAutoPlacements + }) : placement); + }, []); + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var checksMap = new Map(); + var makeFallbackChecks = true; + var firstFittingPlacement = placements[0]; + + for (var i = 0; i < placements.length; i++) { + var placement = placements[i]; + + var _basePlacement = getBasePlacement(placement); + + var isStartVariation = getVariation(placement) === start; + var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; + var len = isVertical ? 'width' : 'height'; + var overflow = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + altBoundary: altBoundary, + padding: padding + }); + var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; + + if (referenceRect[len] > popperRect[len]) { + mainVariationSide = getOppositePlacement(mainVariationSide); + } + + var altVariationSide = getOppositePlacement(mainVariationSide); + var checks = []; + + if (checkMainAxis) { + checks.push(overflow[_basePlacement] <= 0); + } + + if (checkAltAxis) { + checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); + } + + if (checks.every(function (check) { + return check; + })) { + firstFittingPlacement = placement; + makeFallbackChecks = false; + break; + } + + checksMap.set(placement, checks); + } + + if (makeFallbackChecks) { + // `2` may be desired in some cases – research later + var numberOfChecks = flipVariations ? 3 : 1; + + var _loop = function _loop(_i) { + var fittingPlacement = placements.find(function (placement) { + var checks = checksMap.get(placement); + + if (checks) { + return checks.slice(0, _i).every(function (check) { + return check; + }); + } + }); + + if (fittingPlacement) { + firstFittingPlacement = fittingPlacement; + return "break"; + } + }; + + for (var _i = numberOfChecks; _i > 0; _i--) { + var _ret = _loop(_i); + + if (_ret === "break") break; + } + } + + if (state.placement !== firstFittingPlacement) { + state.modifiersData[name]._skip = true; + state.placement = firstFittingPlacement; + state.reset = true; + } + } // eslint-disable-next-line import/no-unused-modules + + + const flip$1 = { + name: 'flip', + enabled: true, + phase: 'main', + fn: flip, + requiresIfExists: ['offset'], + data: { + _skip: false + } + }; + + function getSideOffsets(overflow, rect, preventedOffsets) { + if (preventedOffsets === void 0) { + preventedOffsets = { + x: 0, + y: 0 + }; + } + + return { + top: overflow.top - rect.height - preventedOffsets.y, + right: overflow.right - rect.width + preventedOffsets.x, + bottom: overflow.bottom - rect.height + preventedOffsets.y, + left: overflow.left - rect.width - preventedOffsets.x + }; + } + + function isAnySideFullyClipped(overflow) { + return [top, right, bottom, left].some(function (side) { + return overflow[side] >= 0; + }); + } + + function hide(_ref) { + var state = _ref.state, + name = _ref.name; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var preventedOffsets = state.modifiersData.preventOverflow; + var referenceOverflow = detectOverflow(state, { + elementContext: 'reference' + }); + var popperAltOverflow = detectOverflow(state, { + altBoundary: true + }); + var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); + var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); + var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); + var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); + state.modifiersData[name] = { + referenceClippingOffsets: referenceClippingOffsets, + popperEscapeOffsets: popperEscapeOffsets, + isReferenceHidden: isReferenceHidden, + hasPopperEscaped: hasPopperEscaped + }; + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-reference-hidden': isReferenceHidden, + 'data-popper-escaped': hasPopperEscaped + }); + } // eslint-disable-next-line import/no-unused-modules + + + const hide$1 = { + name: 'hide', + enabled: true, + phase: 'main', + requiresIfExists: ['preventOverflow'], + fn: hide + }; + + function distanceAndSkiddingToXY(placement, rects, offset) { + var basePlacement = getBasePlacement(placement); + var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; + + var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, { + placement: placement + })) : offset, + skidding = _ref[0], + distance = _ref[1]; + + skidding = skidding || 0; + distance = (distance || 0) * invertDistance; + return [left, right].indexOf(basePlacement) >= 0 ? { + x: distance, + y: skidding + } : { + x: skidding, + y: distance + }; + } + + function offset(_ref2) { + var state = _ref2.state, + options = _ref2.options, + name = _ref2.name; + var _options$offset = options.offset, + offset = _options$offset === void 0 ? [0, 0] : _options$offset; + var data = placements.reduce(function (acc, placement) { + acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset); + return acc; + }, {}); + var _data$state$placement = data[state.placement], + x = _data$state$placement.x, + y = _data$state$placement.y; + + if (state.modifiersData.popperOffsets != null) { + state.modifiersData.popperOffsets.x += x; + state.modifiersData.popperOffsets.y += y; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + const offset$1 = { + name: 'offset', + enabled: true, + phase: 'main', + requires: ['popperOffsets'], + fn: offset + }; + + function popperOffsets(_ref) { + var state = _ref.state, + name = _ref.name; + // Offsets are the actual position the popper needs to have to be + // properly positioned near its reference element + // This is the most basic placement, and will be adjusted by + // the modifiers in the next step + state.modifiersData[name] = computeOffsets({ + reference: state.rects.reference, + element: state.rects.popper, + strategy: 'absolute', + placement: state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + const popperOffsets$1 = { + name: 'popperOffsets', + enabled: true, + phase: 'read', + fn: popperOffsets, + data: {} + }; + + function getAltAxis(axis) { + return axis === 'x' ? 'y' : 'x'; + } + + function preventOverflow(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + padding = options.padding, + _options$tether = options.tether, + tether = _options$tether === void 0 ? true : _options$tether, + _options$tetherOffset = options.tetherOffset, + tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; + var overflow = detectOverflow(state, { + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + altBoundary: altBoundary + }); + var basePlacement = getBasePlacement(state.placement); + var variation = getVariation(state.placement); + var isBasePlacement = !variation; + var mainAxis = getMainAxisFromPlacement(basePlacement); + var altAxis = getAltAxis(mainAxis); + var popperOffsets = state.modifiersData.popperOffsets; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, { + placement: state.placement + })) : tetherOffset; + var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? { + mainAxis: tetherOffsetValue, + altAxis: tetherOffsetValue + } : Object.assign({ + mainAxis: 0, + altAxis: 0 + }, tetherOffsetValue); + var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null; + var data = { + x: 0, + y: 0 + }; + + if (!popperOffsets) { + return; + } + + if (checkMainAxis) { + var _offsetModifierState$; + + var mainSide = mainAxis === 'y' ? top : left; + var altSide = mainAxis === 'y' ? bottom : right; + var len = mainAxis === 'y' ? 'height' : 'width'; + var offset = popperOffsets[mainAxis]; + var min$1 = offset + overflow[mainSide]; + var max$1 = offset - overflow[altSide]; + var additive = tether ? -popperRect[len] / 2 : 0; + var minLen = variation === start ? referenceRect[len] : popperRect[len]; + var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go + // outside the reference bounds + + var arrowElement = state.elements.arrow; + var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { + width: 0, + height: 0 + }; + var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject(); + var arrowPaddingMin = arrowPaddingObject[mainSide]; + var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want + // to include its full size in the calculation. If the reference is small + // and near the edge of a boundary, the popper can overflow even if the + // reference is not overflowing as well (e.g. virtual elements with no + // width or height) + + var arrowLen = within(0, referenceRect[len], arrowRect[len]); + var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis; + var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis; + var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); + var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; + var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0; + var tetherMin = offset + minOffset - offsetModifierValue - clientOffset; + var tetherMax = offset + maxOffset - offsetModifierValue; + var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1); + popperOffsets[mainAxis] = preventedOffset; + data[mainAxis] = preventedOffset - offset; + } + + if (checkAltAxis) { + var _offsetModifierState$2; + + var _mainSide = mainAxis === 'x' ? top : left; + + var _altSide = mainAxis === 'x' ? bottom : right; + + var _offset = popperOffsets[altAxis]; + + var _len = altAxis === 'y' ? 'height' : 'width'; + + var _min = _offset + overflow[_mainSide]; + + var _max = _offset - overflow[_altSide]; + + var isOriginSide = [top, left].indexOf(basePlacement) !== -1; + + var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0; + + var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis; + + var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max; + + var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max); + + popperOffsets[altAxis] = _preventedOffset; + data[altAxis] = _preventedOffset - _offset; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + const preventOverflow$1 = { + name: 'preventOverflow', + enabled: true, + phase: 'main', + fn: preventOverflow, + requiresIfExists: ['offset'] + }; + + function getHTMLElementScroll(element) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + + function getNodeScroll(node) { + if (node === getWindow(node) || !isHTMLElement(node)) { + return getWindowScroll(node); + } else { + return getHTMLElementScroll(node); + } + } + + function isElementScaled(element) { + var rect = element.getBoundingClientRect(); + var scaleX = round(rect.width) / element.offsetWidth || 1; + var scaleY = round(rect.height) / element.offsetHeight || 1; + return scaleX !== 1 || scaleY !== 1; + } // Returns the composite rect of an element relative to its offsetParent. + // Composite means it takes into account transforms as well as layout. + + + function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { + if (isFixed === void 0) { + isFixed = false; + } + + var isOffsetParentAnElement = isHTMLElement(offsetParent); + var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent); + var documentElement = getDocumentElement(offsetParent); + var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed); + var scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + var offsets = { + x: 0, + y: 0 + }; + + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078 + isScrollParent(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + + if (isHTMLElement(offsetParent)) { + offsets = getBoundingClientRect(offsetParent, true); + offsets.x += offsetParent.clientLeft; + offsets.y += offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; + } + + function order(modifiers) { + var map = new Map(); + var visited = new Set(); + var result = []; + modifiers.forEach(function (modifier) { + map.set(modifier.name, modifier); + }); // On visiting object, check for its dependencies and visit them recursively + + function sort(modifier) { + visited.add(modifier.name); + var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); + requires.forEach(function (dep) { + if (!visited.has(dep)) { + var depModifier = map.get(dep); + + if (depModifier) { + sort(depModifier); + } + } + }); + result.push(modifier); + } + + modifiers.forEach(function (modifier) { + if (!visited.has(modifier.name)) { + // check for visited object + sort(modifier); + } + }); + return result; + } + + function orderModifiers(modifiers) { + // order based on dependencies + var orderedModifiers = order(modifiers); // order based on phase + + return modifierPhases.reduce(function (acc, phase) { + return acc.concat(orderedModifiers.filter(function (modifier) { + return modifier.phase === phase; + })); + }, []); + } + + function debounce(fn) { + var pending; + return function () { + if (!pending) { + pending = new Promise(function (resolve) { + Promise.resolve().then(function () { + pending = undefined; + resolve(fn()); + }); + }); + } + + return pending; + }; + } + + function mergeByName(modifiers) { + var merged = modifiers.reduce(function (merged, current) { + var existing = merged[current.name]; + merged[current.name] = existing ? Object.assign({}, existing, current, { + options: Object.assign({}, existing.options, current.options), + data: Object.assign({}, existing.data, current.data) + }) : current; + return merged; + }, {}); // IE11 does not support Object.values + + return Object.keys(merged).map(function (key) { + return merged[key]; + }); + } + + var DEFAULT_OPTIONS = { + placement: 'bottom', + modifiers: [], + strategy: 'absolute' + }; + + function areValidElements() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return !args.some(function (element) { + return !(element && typeof element.getBoundingClientRect === 'function'); + }); + } + + function popperGenerator(generatorOptions) { + if (generatorOptions === void 0) { + generatorOptions = {}; + } + + var _generatorOptions = generatorOptions, + _generatorOptions$def = _generatorOptions.defaultModifiers, + defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, + _generatorOptions$def2 = _generatorOptions.defaultOptions, + defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; + return function createPopper(reference, popper, options) { + if (options === void 0) { + options = defaultOptions; + } + + var state = { + placement: 'bottom', + orderedModifiers: [], + options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), + modifiersData: {}, + elements: { + reference: reference, + popper: popper + }, + attributes: {}, + styles: {} + }; + var effectCleanupFns = []; + var isDestroyed = false; + var instance = { + state: state, + setOptions: function setOptions(setOptionsAction) { + var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction; + cleanupModifierEffects(); + state.options = Object.assign({}, defaultOptions, state.options, options); + state.scrollParents = { + reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [], + popper: listScrollParents(popper) + }; // Orders the modifiers based on their dependencies and `phase` + // properties + + var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers + + state.orderedModifiers = orderedModifiers.filter(function (m) { + return m.enabled; + }); + runModifierEffects(); + return instance.update(); + }, + // Sync update – it will always be executed, even if not necessary. This + // is useful for low frequency updates where sync behavior simplifies the + // logic. + // For high frequency updates (e.g. `resize` and `scroll` events), always + // prefer the async Popper#update method + forceUpdate: function forceUpdate() { + if (isDestroyed) { + return; + } + + var _state$elements = state.elements, + reference = _state$elements.reference, + popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements + // anymore + + if (!areValidElements(reference, popper)) { + return; + } // Store the reference and popper rects to be read by modifiers + + + state.rects = { + reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'), + popper: getLayoutRect(popper) + }; // Modifiers have the ability to reset the current update cycle. The + // most common use case for this is the `flip` modifier changing the + // placement, which then needs to re-run all the modifiers, because the + // logic was previously ran for the previous placement and is therefore + // stale/incorrect + + state.reset = false; + state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier + // is filled with the initial data specified by the modifier. This means + // it doesn't persist and is fresh on each update. + // To ensure persistent data, use `${name}#persistent` + + state.orderedModifiers.forEach(function (modifier) { + return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); + }); + + for (var index = 0; index < state.orderedModifiers.length; index++) { + if (state.reset === true) { + state.reset = false; + index = -1; + continue; + } + + var _state$orderedModifie = state.orderedModifiers[index], + fn = _state$orderedModifie.fn, + _state$orderedModifie2 = _state$orderedModifie.options, + _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, + name = _state$orderedModifie.name; + + if (typeof fn === 'function') { + state = fn({ + state: state, + options: _options, + name: name, + instance: instance + }) || state; + } + } + }, + // Async and optimistically optimized update – it will not be executed if + // not necessary (debounced to run at most once-per-tick) + update: debounce(function () { + return new Promise(function (resolve) { + instance.forceUpdate(); + resolve(state); + }); + }), + destroy: function destroy() { + cleanupModifierEffects(); + isDestroyed = true; + } + }; + + if (!areValidElements(reference, popper)) { + return instance; + } + + instance.setOptions(options).then(function (state) { + if (!isDestroyed && options.onFirstUpdate) { + options.onFirstUpdate(state); + } + }); // Modifiers have the ability to execute arbitrary code before the first + // update cycle runs. They will be executed in the same order as the update + // cycle. This is useful when a modifier adds some persistent data that + // other modifiers need to use, but the modifier is run after the dependent + // one. + + function runModifierEffects() { + state.orderedModifiers.forEach(function (_ref) { + var name = _ref.name, + _ref$options = _ref.options, + options = _ref$options === void 0 ? {} : _ref$options, + effect = _ref.effect; + + if (typeof effect === 'function') { + var cleanupFn = effect({ + state: state, + name: name, + instance: instance, + options: options + }); + + var noopFn = function noopFn() {}; + + effectCleanupFns.push(cleanupFn || noopFn); + } + }); + } + + function cleanupModifierEffects() { + effectCleanupFns.forEach(function (fn) { + return fn(); + }); + effectCleanupFns = []; + } + + return instance; + }; + } + var createPopper$2 = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules + + var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1]; + var createPopper$1 = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers$1 + }); // eslint-disable-next-line import/no-unused-modules + + var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1]; + var createPopper = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers + }); // eslint-disable-next-line import/no-unused-modules + + const Popper = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ + __proto__: null, + afterMain, + afterRead, + afterWrite, + applyStyles: applyStyles$1, + arrow: arrow$1, + auto, + basePlacements, + beforeMain, + beforeRead, + beforeWrite, + bottom, + clippingParents, + computeStyles: computeStyles$1, + createPopper, + createPopperBase: createPopper$2, + createPopperLite: createPopper$1, + detectOverflow, + end, + eventListeners, + flip: flip$1, + hide: hide$1, + left, + main, + modifierPhases, + offset: offset$1, + placements, + popper, + popperGenerator, + popperOffsets: popperOffsets$1, + preventOverflow: preventOverflow$1, + read, + reference, + right, + start, + top, + variationPlacements, + viewport, + write + }, Symbol.toStringTag, { value: 'Module' })); + + /** + * -------------------------------------------------------------------------- + * Bootstrap dropdown.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$a = 'dropdown'; + const DATA_KEY$6 = 'bs.dropdown'; + const EVENT_KEY$6 = `.${DATA_KEY$6}`; + const DATA_API_KEY$3 = '.data-api'; + const ESCAPE_KEY$2 = 'Escape'; + const TAB_KEY$1 = 'Tab'; + const ARROW_UP_KEY$1 = 'ArrowUp'; + const ARROW_DOWN_KEY$1 = 'ArrowDown'; + const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button + + const EVENT_HIDE$5 = `hide${EVENT_KEY$6}`; + const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`; + const EVENT_SHOW$5 = `show${EVENT_KEY$6}`; + const EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`; + const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`; + const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`; + const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`; + const CLASS_NAME_SHOW$6 = 'show'; + const CLASS_NAME_DROPUP = 'dropup'; + const CLASS_NAME_DROPEND = 'dropend'; + const CLASS_NAME_DROPSTART = 'dropstart'; + const CLASS_NAME_DROPUP_CENTER = 'dropup-center'; + const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'; + const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'; + const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`; + const SELECTOR_MENU = '.dropdown-menu'; + const SELECTOR_NAVBAR = '.navbar'; + const SELECTOR_NAVBAR_NAV = '.navbar-nav'; + const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; + const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'; + const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'; + const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'; + const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'; + const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'; + const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'; + const PLACEMENT_TOPCENTER = 'top'; + const PLACEMENT_BOTTOMCENTER = 'bottom'; + const Default$9 = { + autoClose: true, + boundary: 'clippingParents', + display: 'dynamic', + offset: [0, 2], + popperConfig: null, + reference: 'toggle' + }; + const DefaultType$9 = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + display: 'string', + offset: '(array|string|function)', + popperConfig: '(null|object|function)', + reference: '(string|element|object)' + }; + + /** + * Class definition + */ + + class Dropdown extends BaseComponent { + constructor(element, config) { + super(element, config); + this._popper = null; + this._parent = this._element.parentNode; // dropdown wrapper + // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ + this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent); + this._inNavbar = this._detectNavbar(); + } + + // Getters + static get Default() { + return Default$9; + } + static get DefaultType() { + return DefaultType$9; + } + static get NAME() { + return NAME$a; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._createPopper(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of [].concat(...document.body.children)) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus(); + this._element.setAttribute('aria-expanded', true); + this._menu.classList.add(CLASS_NAME_SHOW$6); + this._element.classList.add(CLASS_NAME_SHOW$6); + EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + if (this._popper) { + this._popper.destroy(); + } + super.dispose(); + } + update() { + this._inNavbar = this._detectNavbar(); + if (this._popper) { + this._popper.update(); + } + } + + // Private + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of [].concat(...document.body.children)) { + EventHandler.off(element, 'mouseover', noop); + } + } + if (this._popper) { + this._popper.destroy(); + } + this._menu.classList.remove(CLASS_NAME_SHOW$6); + this._element.classList.remove(CLASS_NAME_SHOW$6); + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'popper'); + EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement$1(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + // Popper virtual elements require a getBoundingClientRect method + throw new TypeError(`${NAME$a.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createPopper() { + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)'); + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement$1(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + const popperConfig = this._getPopperConfig(); + this._popper = createPopper(referenceElement, this._menu, popperConfig); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$6); + } + _getPlacement() { + const parentDropdown = this._parent; + if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { + return PLACEMENT_RIGHT; + } + if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) { + return PLACEMENT_LEFT; + } + if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) { + return PLACEMENT_TOPCENTER; + } + if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) { + return PLACEMENT_BOTTOMCENTER; + } + + // We need to trim the value because custom properties can also include spaces + const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'; + if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) { + return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP; + } + return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM; + } + _detectNavbar() { + return this._element.closest(SELECTOR_NAVBAR) !== null; + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + return popperData => offset(popperData, this._element); + } + return offset; + } + _getPopperConfig() { + const defaultBsPopperConfig = { + placement: this._getPlacement(), + modifiers: [{ + name: 'preventOverflow', + options: { + boundary: this._config.boundary + } + }, { + name: 'offset', + options: { + offset: this._getOffset() + } + }] + }; + + // Disable Popper if we have a static display or Dropdown is in Navbar + if (this._inNavbar || this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove + defaultBsPopperConfig.modifiers = [{ + name: 'applyStyles', + enabled: false + }]; + } + return { + ...defaultBsPopperConfig, + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) + }; + } + _selectMenuItem({ + key, + target + }) { + const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + + // if target isn't included in items (e.g. when expanding the dropdown) + // allow cycling to get the last item in case key equals ARROW_UP_KEY + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Dropdown.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { + return; + } + const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN); + for (const toggle of openToggles) { + const context = Dropdown.getInstance(toggle); + if (!context || context._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(context._menu); + if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu + if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) { + continue; + } + const relatedTarget = { + relatedTarget: context._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + context._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // If not an UP | DOWN | ESCAPE key => not a dropdown command + // If input/textarea && if key is other than ESCAPE => not a dropdown command + + const isInput = /input|textarea/i.test(event.target.tagName); + const isEscapeEvent = event.key === ESCAPE_KEY$2; + const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key); + if (!isUpOrDownEvent && !isEscapeEvent) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + event.preventDefault(); + + // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ + const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode); + const instance = Dropdown.getOrCreateInstance(getToggleButton); + if (isUpOrDownEvent) { + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (instance._isShown()) { + // else is escape and we check if it is shown + event.stopPropagation(); + instance.hide(); + getToggleButton.focus(); + } + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler); + EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler); + EventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus); + EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus); + EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) { + event.preventDefault(); + Dropdown.getOrCreateInstance(this).toggle(); + }); + + /** + * jQuery + */ + + defineJQueryPlugin(Dropdown); + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/backdrop.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$9 = 'backdrop'; + const CLASS_NAME_FADE$4 = 'fade'; + const CLASS_NAME_SHOW$5 = 'show'; + const EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`; + const Default$8 = { + className: 'modal-backdrop', + clickCallback: null, + isAnimated: false, + isVisible: true, + // if false, we use the backdrop helper without adding any element to the dom + rootElement: 'body' // give the choice to place backdrop under different elements + }; + const DefaultType$8 = { + className: 'string', + clickCallback: '(function|null)', + isAnimated: 'boolean', + isVisible: 'boolean', + rootElement: '(element|string)' + }; + + /** + * Class definition + */ + + class Backdrop extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + this._isAppended = false; + this._element = null; + } + + // Getters + static get Default() { + return Default$8; + } + static get DefaultType() { + return DefaultType$8; + } + static get NAME() { + return NAME$9; + } + + // Public + show(callback) { + if (!this._config.isVisible) { + execute(callback); + return; + } + this._append(); + const element = this._getElement(); + if (this._config.isAnimated) { + reflow(element); + } + element.classList.add(CLASS_NAME_SHOW$5); + this._emulateAnimation(() => { + execute(callback); + }); + } + hide(callback) { + if (!this._config.isVisible) { + execute(callback); + return; + } + this._getElement().classList.remove(CLASS_NAME_SHOW$5); + this._emulateAnimation(() => { + this.dispose(); + execute(callback); + }); + } + dispose() { + if (!this._isAppended) { + return; + } + EventHandler.off(this._element, EVENT_MOUSEDOWN); + this._element.remove(); + this._isAppended = false; + } + + // Private + _getElement() { + if (!this._element) { + const backdrop = document.createElement('div'); + backdrop.className = this._config.className; + if (this._config.isAnimated) { + backdrop.classList.add(CLASS_NAME_FADE$4); + } + this._element = backdrop; + } + return this._element; + } + _configAfterMerge(config) { + // use getElement() with the default "body" to get a fresh Element on each instantiation + config.rootElement = getElement(config.rootElement); + return config; + } + _append() { + if (this._isAppended) { + return; + } + const element = this._getElement(); + this._config.rootElement.append(element); + EventHandler.on(element, EVENT_MOUSEDOWN, () => { + execute(this._config.clickCallback); + }); + this._isAppended = true; + } + _emulateAnimation(callback) { + executeAfterTransition(callback, this._getElement(), this._config.isAnimated); + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/focustrap.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$8 = 'focustrap'; + const DATA_KEY$5 = 'bs.focustrap'; + const EVENT_KEY$5 = `.${DATA_KEY$5}`; + const EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`; + const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`; + const TAB_KEY = 'Tab'; + const TAB_NAV_FORWARD = 'forward'; + const TAB_NAV_BACKWARD = 'backward'; + const Default$7 = { + autofocus: true, + trapElement: null // The element to trap focus inside of + }; + const DefaultType$7 = { + autofocus: 'boolean', + trapElement: 'element' + }; + + /** + * Class definition + */ + + class FocusTrap extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + this._isActive = false; + this._lastTabNavDirection = null; + } + + // Getters + static get Default() { + return Default$7; + } + static get DefaultType() { + return DefaultType$7; + } + static get NAME() { + return NAME$8; + } + + // Public + activate() { + if (this._isActive) { + return; + } + if (this._config.autofocus) { + this._config.trapElement.focus(); + } + EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop + EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event)); + EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)); + this._isActive = true; + } + deactivate() { + if (!this._isActive) { + return; + } + this._isActive = false; + EventHandler.off(document, EVENT_KEY$5); + } + + // Private + _handleFocusin(event) { + const { + trapElement + } = this._config; + if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { + return; + } + const elements = SelectorEngine.focusableChildren(trapElement); + if (elements.length === 0) { + trapElement.focus(); + } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { + elements[elements.length - 1].focus(); + } else { + elements[0].focus(); + } + } + _handleKeydown(event) { + if (event.key !== TAB_KEY) { + return; + } + this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD; + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/scrollBar.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; + const SELECTOR_STICKY_CONTENT = '.sticky-top'; + const PROPERTY_PADDING = 'padding-right'; + const PROPERTY_MARGIN = 'margin-right'; + + /** + * Class definition + */ + + class ScrollBarHelper { + constructor() { + this._element = document.body; + } + + // Public + getWidth() { + // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes + const documentWidth = document.documentElement.clientWidth; + return Math.abs(window.innerWidth - documentWidth); + } + hide() { + const width = this.getWidth(); + this._disableOverFlow(); + // give padding to element to balance the hidden scrollbar width + this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); + // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth + this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width); + this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width); + } + reset() { + this._resetElementAttributes(this._element, 'overflow'); + this._resetElementAttributes(this._element, PROPERTY_PADDING); + this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING); + this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN); + } + isOverflowing() { + return this.getWidth() > 0; + } + + // Private + _disableOverFlow() { + this._saveInitialAttribute(this._element, 'overflow'); + this._element.style.overflow = 'hidden'; + } + _setElementAttributes(selector, styleProperty, callback) { + const scrollbarWidth = this.getWidth(); + const manipulationCallBack = element => { + if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { + return; + } + this._saveInitialAttribute(element, styleProperty); + const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty); + element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`); + }; + this._applyManipulationCallback(selector, manipulationCallBack); + } + _saveInitialAttribute(element, styleProperty) { + const actualValue = element.style.getPropertyValue(styleProperty); + if (actualValue) { + Manipulator.setDataAttribute(element, styleProperty, actualValue); + } + } + _resetElementAttributes(selector, styleProperty) { + const manipulationCallBack = element => { + const value = Manipulator.getDataAttribute(element, styleProperty); + // We only want to remove the property if the value is `null`; the value can also be zero + if (value === null) { + element.style.removeProperty(styleProperty); + return; + } + Manipulator.removeDataAttribute(element, styleProperty); + element.style.setProperty(styleProperty, value); + }; + this._applyManipulationCallback(selector, manipulationCallBack); + } + _applyManipulationCallback(selector, callBack) { + if (isElement$1(selector)) { + callBack(selector); + return; + } + for (const sel of SelectorEngine.find(selector, this._element)) { + callBack(sel); + } + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap modal.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$7 = 'modal'; + const DATA_KEY$4 = 'bs.modal'; + const EVENT_KEY$4 = `.${DATA_KEY$4}`; + const DATA_API_KEY$2 = '.data-api'; + const ESCAPE_KEY$1 = 'Escape'; + const EVENT_HIDE$4 = `hide${EVENT_KEY$4}`; + const EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`; + const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`; + const EVENT_SHOW$4 = `show${EVENT_KEY$4}`; + const EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`; + const EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`; + const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`; + const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`; + const EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`; + const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`; + const CLASS_NAME_OPEN = 'modal-open'; + const CLASS_NAME_FADE$3 = 'fade'; + const CLASS_NAME_SHOW$4 = 'show'; + const CLASS_NAME_STATIC = 'modal-static'; + const OPEN_SELECTOR$1 = '.modal.show'; + const SELECTOR_DIALOG = '.modal-dialog'; + const SELECTOR_MODAL_BODY = '.modal-body'; + const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="modal"]'; + const Default$6 = { + backdrop: true, + focus: true, + keyboard: true + }; + const DefaultType$6 = { + backdrop: '(boolean|string)', + focus: 'boolean', + keyboard: 'boolean' + }; + + /** + * Class definition + */ + + class Modal extends BaseComponent { + constructor(element, config) { + super(element, config); + this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element); + this._backdrop = this._initializeBackDrop(); + this._focustrap = this._initializeFocusTrap(); + this._isShown = false; + this._isTransitioning = false; + this._scrollBar = new ScrollBarHelper(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$6; + } + static get DefaultType() { + return DefaultType$6; + } + static get NAME() { + return NAME$7; + } + + // Public + toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._isShown || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isShown = true; + this._isTransitioning = true; + this._scrollBar.hide(); + document.body.classList.add(CLASS_NAME_OPEN); + this._adjustDialog(); + this._backdrop.show(() => this._showElement(relatedTarget)); + } + hide() { + if (!this._isShown || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4); + if (hideEvent.defaultPrevented) { + return; + } + this._isShown = false; + this._isTransitioning = true; + this._focustrap.deactivate(); + this._element.classList.remove(CLASS_NAME_SHOW$4); + this._queueCallback(() => this._hideModal(), this._element, this._isAnimated()); + } + dispose() { + EventHandler.off(window, EVENT_KEY$4); + EventHandler.off(this._dialog, EVENT_KEY$4); + this._backdrop.dispose(); + this._focustrap.deactivate(); + super.dispose(); + } + handleUpdate() { + this._adjustDialog(); + } + + // Private + _initializeBackDrop() { + return new Backdrop({ + isVisible: Boolean(this._config.backdrop), + // 'static' option will be translated to true, and booleans will keep their value, + isAnimated: this._isAnimated() + }); + } + _initializeFocusTrap() { + return new FocusTrap({ + trapElement: this._element + }); + } + _showElement(relatedTarget) { + // try to append dynamic modal + if (!document.body.contains(this._element)) { + document.body.append(this._element); + } + this._element.style.display = 'block'; + this._element.removeAttribute('aria-hidden'); + this._element.setAttribute('aria-modal', true); + this._element.setAttribute('role', 'dialog'); + this._element.scrollTop = 0; + const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog); + if (modalBody) { + modalBody.scrollTop = 0; + } + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW$4); + const transitionComplete = () => { + if (this._config.focus) { + this._focustrap.activate(); + } + this._isTransitioning = false; + EventHandler.trigger(this._element, EVENT_SHOWN$4, { + relatedTarget + }); + }; + this._queueCallback(transitionComplete, this._dialog, this._isAnimated()); + } + _addEventListeners() { + EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => { + if (event.key !== ESCAPE_KEY$1) { + return; + } + if (this._config.keyboard) { + this.hide(); + return; + } + this._triggerBackdropTransition(); + }); + EventHandler.on(window, EVENT_RESIZE$1, () => { + if (this._isShown && !this._isTransitioning) { + this._adjustDialog(); + } + }); + EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => { + // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks + EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => { + if (this._element !== event.target || this._element !== event2.target) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; + } + if (this._config.backdrop) { + this.hide(); + } + }); + }); + } + _hideModal() { + this._element.style.display = 'none'; + this._element.setAttribute('aria-hidden', true); + this._element.removeAttribute('aria-modal'); + this._element.removeAttribute('role'); + this._isTransitioning = false; + this._backdrop.hide(() => { + document.body.classList.remove(CLASS_NAME_OPEN); + this._resetAdjustments(); + this._scrollBar.reset(); + EventHandler.trigger(this._element, EVENT_HIDDEN$4); + }); + } + _isAnimated() { + return this._element.classList.contains(CLASS_NAME_FADE$3); + } + _triggerBackdropTransition() { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1); + if (hideEvent.defaultPrevented) { + return; + } + const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; + const initialOverflowY = this._element.style.overflowY; + // return if the following background transition hasn't yet completed + if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { + return; + } + if (!isModalOverflowing) { + this._element.style.overflowY = 'hidden'; + } + this._element.classList.add(CLASS_NAME_STATIC); + this._queueCallback(() => { + this._element.classList.remove(CLASS_NAME_STATIC); + this._queueCallback(() => { + this._element.style.overflowY = initialOverflowY; + }, this._dialog); + }, this._dialog); + this._element.focus(); + } + + /** + * The following methods are used to handle overflowing modals + */ + + _adjustDialog() { + const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; + const scrollbarWidth = this._scrollBar.getWidth(); + const isBodyOverflowing = scrollbarWidth > 0; + if (isBodyOverflowing && !isModalOverflowing) { + const property = isRTL() ? 'paddingLeft' : 'paddingRight'; + this._element.style[property] = `${scrollbarWidth}px`; + } + if (!isBodyOverflowing && isModalOverflowing) { + const property = isRTL() ? 'paddingRight' : 'paddingLeft'; + this._element.style[property] = `${scrollbarWidth}px`; + } + } + _resetAdjustments() { + this._element.style.paddingLeft = ''; + this._element.style.paddingRight = ''; + } + + // Static + static jQueryInterface(config, relatedTarget) { + return this.each(function () { + const data = Modal.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](relatedTarget); + }); + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW$4, showEvent => { + if (showEvent.defaultPrevented) { + // only register focus restorer if modal will actually get shown + return; + } + EventHandler.one(target, EVENT_HIDDEN$4, () => { + if (isVisible(this)) { + this.focus(); + } + }); + }); + + // avoid conflict when clicking modal toggler while another one is open + const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1); + if (alreadyOpen) { + Modal.getInstance(alreadyOpen).hide(); + } + const data = Modal.getOrCreateInstance(target); + data.toggle(this); + }); + enableDismissTrigger(Modal); + + /** + * jQuery + */ + + defineJQueryPlugin(Modal); + + /** + * -------------------------------------------------------------------------- + * Bootstrap offcanvas.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$6 = 'offcanvas'; + const DATA_KEY$3 = 'bs.offcanvas'; + const EVENT_KEY$3 = `.${DATA_KEY$3}`; + const DATA_API_KEY$1 = '.data-api'; + const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`; + const ESCAPE_KEY = 'Escape'; + const CLASS_NAME_SHOW$3 = 'show'; + const CLASS_NAME_SHOWING$1 = 'showing'; + const CLASS_NAME_HIDING = 'hiding'; + const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'; + const OPEN_SELECTOR = '.offcanvas.show'; + const EVENT_SHOW$3 = `show${EVENT_KEY$3}`; + const EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`; + const EVENT_HIDE$3 = `hide${EVENT_KEY$3}`; + const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`; + const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`; + const EVENT_RESIZE = `resize${EVENT_KEY$3}`; + const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`; + const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`; + const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="offcanvas"]'; + const Default$5 = { + backdrop: true, + keyboard: true, + scroll: false + }; + const DefaultType$5 = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' + }; + + /** + * Class definition + */ + + class Offcanvas extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isShown = false; + this._backdrop = this._initializeBackDrop(); + this._focustrap = this._initializeFocusTrap(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$5; + } + static get DefaultType() { + return DefaultType$5; + } + static get NAME() { + return NAME$6; + } + + // Public + toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isShown = true; + this._backdrop.show(); + if (!this._config.scroll) { + new ScrollBarHelper().hide(); + } + this._element.setAttribute('aria-modal', true); + this._element.setAttribute('role', 'dialog'); + this._element.classList.add(CLASS_NAME_SHOWING$1); + const completeCallBack = () => { + if (!this._config.scroll || this._config.backdrop) { + this._focustrap.activate(); + } + this._element.classList.add(CLASS_NAME_SHOW$3); + this._element.classList.remove(CLASS_NAME_SHOWING$1); + EventHandler.trigger(this._element, EVENT_SHOWN$3, { + relatedTarget + }); + }; + this._queueCallback(completeCallBack, this._element, true); + } + hide() { + if (!this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); + if (hideEvent.defaultPrevented) { + return; + } + this._focustrap.deactivate(); + this._element.blur(); + this._isShown = false; + this._element.classList.add(CLASS_NAME_HIDING); + this._backdrop.hide(); + const completeCallback = () => { + this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING); + this._element.removeAttribute('aria-modal'); + this._element.removeAttribute('role'); + if (!this._config.scroll) { + new ScrollBarHelper().reset(); + } + EventHandler.trigger(this._element, EVENT_HIDDEN$3); + }; + this._queueCallback(completeCallback, this._element, true); + } + dispose() { + this._backdrop.dispose(); + this._focustrap.deactivate(); + super.dispose(); + } + + // Private + _initializeBackDrop() { + const clickCallback = () => { + if (this._config.backdrop === 'static') { + EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); + return; + } + this.hide(); + }; + + // 'static' option will be translated to true, and booleans will keep their value + const isVisible = Boolean(this._config.backdrop); + return new Backdrop({ + className: CLASS_NAME_BACKDROP, + isVisible, + isAnimated: true, + rootElement: this._element.parentNode, + clickCallback: isVisible ? clickCallback : null + }); + } + _initializeFocusTrap() { + return new FocusTrap({ + trapElement: this._element + }); + } + _addEventListeners() { + EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { + if (event.key !== ESCAPE_KEY) { + return; + } + if (this._config.keyboard) { + this.hide(); + return; + } + EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); + }); + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Offcanvas.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { + throw new TypeError(`No method named "${config}"`); + } + data[config](this); + }); + } + } + + /** + * Data API implementation + */ + + EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$3, () => { + // focus on trigger when it is closed + if (isVisible(this)) { + this.focus(); + } + }); + + // avoid conflict when clicking a toggler of an offcanvas, while another is open + const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); + if (alreadyOpen && alreadyOpen !== target) { + Offcanvas.getInstance(alreadyOpen).hide(); + } + const data = Offcanvas.getOrCreateInstance(target); + data.toggle(this); + }); + EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { + for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { + Offcanvas.getOrCreateInstance(selector).show(); + } + }); + EventHandler.on(window, EVENT_RESIZE, () => { + for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) { + if (getComputedStyle(element).position !== 'fixed') { + Offcanvas.getOrCreateInstance(element).hide(); + } + } + }); + enableDismissTrigger(Offcanvas); + + /** + * jQuery + */ + + defineJQueryPlugin(Offcanvas); + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + // js-docs-start allow-list + const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; + const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], + a: ['target', 'href', 'title', 'rel'], + area: [], + b: [], + br: [], + col: [], + code: [], + dd: [], + div: [], + dl: [], + dt: [], + em: [], + hr: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + i: [], + img: ['src', 'srcset', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [] + }; + // js-docs-end allow-list + + const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); + + /** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ + // eslint-disable-next-line unicorn/better-regex + const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; + const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)); + } + return true; + } + + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); + }; + function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [].concat(...createdDocument.body.querySelectorAll('*')); + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; + } + const attributeList = [].concat(...element.attributes); + const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []); + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); + } + } + } + return createdDocument.body.innerHTML; + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$5 = 'TemplateFactory'; + const Default$4 = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '
' + }; + const DefaultType$4 = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' + }; + const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' + }; + + /** + * Class definition + */ + + class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } + + // Getters + static get Default() { + return Default$4; + } + static get DefaultType() { + return DefaultType$4; + } + static get NAME() { + return NAME$5; + } + + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); + } + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); + } + return template; + } + + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); + } + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; + } + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; + } + if (isElement$1(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; + } + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; + } + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; + } + templateElement.textContent = element.textContent; + } + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$4 = 'tooltip'; + const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); + const CLASS_NAME_FADE$2 = 'fade'; + const CLASS_NAME_MODAL = 'modal'; + const CLASS_NAME_SHOW$2 = 'show'; + const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; + const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; + const EVENT_MODAL_HIDE = 'hide.bs.modal'; + const TRIGGER_HOVER = 'hover'; + const TRIGGER_FOCUS = 'focus'; + const TRIGGER_CLICK = 'click'; + const TRIGGER_MANUAL = 'manual'; + const EVENT_HIDE$2 = 'hide'; + const EVENT_HIDDEN$2 = 'hidden'; + const EVENT_SHOW$2 = 'show'; + const EVENT_SHOWN$2 = 'shown'; + const EVENT_INSERTED = 'inserted'; + const EVENT_CLICK$1 = 'click'; + const EVENT_FOCUSIN$1 = 'focusin'; + const EVENT_FOCUSOUT$1 = 'focusout'; + const EVENT_MOUSEENTER = 'mouseenter'; + const EVENT_MOUSELEAVE = 'mouseleave'; + const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL() ? 'right' : 'left' + }; + const Default$3 = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + popperConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '', + title: '', + trigger: 'hover focus' + }; + const DefaultType$3 = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + popperConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' + }; + + /** + * Class definition + */ + + class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)'); + } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._popper = null; + this._templateFactory = null; + this._newContent = null; + + // Protected + this.tip = null; + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); + } + } + + // Getters + static get Default() { + return Default$3; + } + static get DefaultType() { + return DefaultType$3; + } + static get NAME() { + return NAME$4; + } + + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; + } + this._activeTrigger.click = !this._activeTrigger.click; + if (this._isShown()) { + this._leave(); + return; + } + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposePopper(); + super.dispose(); + } + show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (!(this._isWithContent() && this._isEnabled)) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + return; + } + + // TODO: v6 remove this or make it optional + this._disposePopper(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + const { + container + } = this._config; + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + this._popper = this._createPopper(tip); + tip.classList.add(CLASS_NAME_SHOW$2); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of [].concat(...document.body.children)) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); + if (this._isHovered === false) { + this._leave(); + } + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); + if (hideEvent.defaultPrevented) { + return; + } + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW$2); + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of [].concat(...document.body.children)) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering + + const complete = () => { + if (this._isWithActiveTrigger()) { + return; + } + if (!this._isHovered) { + this._disposePopper(); + } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._popper) { + this._popper.update(); + } + } + + // Protected + _isWithContent() { + return Boolean(this._getTitle()); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); + } + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + + // TODO: remove this check in v6 + if (!tip) { + return null; + } + tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); + // TODO: v6 the following can be achieved with CSS only + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE$2); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposePopper(); + this.show(); + } + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); + } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } + + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } + _createPopper(tip) { + const placement = execute(this._config.placement, [this, tip, this._element]); + const attachment = AttachmentMap[placement.toUpperCase()]; + return createPopper(this._element, tip, this._getPopperConfig(attachment)); + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + return popperData => offset(popperData, this._element); + } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element]); + } + _getPopperConfig(attachment) { + const defaultBsPopperConfig = { + placement: attachment, + modifiers: [{ + name: 'flip', + options: { + fallbackPlacements: this._config.fallbackPlacements + } + }, { + name: 'offset', + options: { + offset: this._getOffset() + } + }, { + name: 'preventOverflow', + options: { + boundary: this._config.boundary + } + }, { + name: 'arrow', + options: { + element: `.${this.constructor.NAME}-arrow` + } + }, { + name: 'preSetPlacement', + enabled: true, + phase: 'beforeMain', + fn: data => { + // Pre-set Popper's placement attribute in order to read the arrow sizes properly. + // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement + this._getTipElement().setAttribute('data-popper-placement', data.state.placement); + } + }] + }; + return { + ...defaultBsPopperConfig, + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); + } + } + this._hideModalHandler = () => { + if (this._element) { + this.hide(); + } + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; + } + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); + } + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { + this._isHovered = true; + return; + } + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { + this.show(); + } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; + } + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); + } + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; + } + } + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + if (typeof config.title === 'number') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number') { + config.content = config.content.toString(); + } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; + } + } + config.selector = false; + config.trigger = 'manual'; + + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposePopper() { + if (this._popper) { + this._popper.destroy(); + this._popper = null; + } + if (this.tip) { + this.tip.remove(); + this.tip = null; + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Tooltip.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } + } + + /** + * jQuery + */ + + defineJQueryPlugin(Tooltip); + + /** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$3 = 'popover'; + const SELECTOR_TITLE = '.popover-header'; + const SELECTOR_CONTENT = '.popover-body'; + const Default$2 = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '', + trigger: 'click' + }; + const DefaultType$2 = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' + }; + + /** + * Class definition + */ + + class Popover extends Tooltip { + // Getters + static get Default() { + return Default$2; + } + static get DefaultType() { + return DefaultType$2; + } + static get NAME() { + return NAME$3; + } + + // Overrides + _isWithContent() { + return this._getTitle() || this._getContent(); + } + + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Popover.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } + } + + /** + * jQuery + */ + + defineJQueryPlugin(Popover); + + /** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + + /** + * Constants + */ + + const NAME$2 = 'scrollspy'; + const DATA_KEY$2 = 'bs.scrollspy'; + const EVENT_KEY$2 = `.${DATA_KEY$2}`; + const DATA_API_KEY = '.data-api'; + const EVENT_ACTIVATE = `activate${EVENT_KEY$2}`; + const EVENT_CLICK = `click${EVENT_KEY$2}`; + const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`; + const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; + const CLASS_NAME_ACTIVE$1 = 'active'; + const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; + const SELECTOR_TARGET_LINKS = '[href]'; + const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; + const SELECTOR_NAV_LINKS = '.nav-link'; + const SELECTOR_NAV_ITEMS = '.nav-item'; + const SELECTOR_LIST_ITEMS = '.list-group-item'; + const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; + const SELECTOR_DROPDOWN = '.dropdown'; + const SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle'; + const Default$1 = { + offset: null, + // TODO: v6 @deprecated, keep it for backwards compatibility reasons + rootMargin: '0px 0px -25%', + smoothScroll: false, + target: null, + threshold: [0.1, 0.5, 1] + }; + const DefaultType$1 = { + offset: '(number|null)', + // TODO v6 @deprecated, keep it for backwards compatibility reasons + rootMargin: 'string', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array' + }; + + /** + * Class definition + */ + + class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._targetLinks = new Map(); + this._observableSections = new Map(); + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._activeTarget = null; + this._observer = null; + this._previousScrollData = { + visibleEntryTop: 0, + parentScrollTop: 0 + }; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default$1; + } + static get DefaultType() { + return DefaultType$1; + } + static get NAME() { + return NAME$2; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + if (this._observer) { + this._observer.disconnect(); + } else { + this._observer = this._getNewObserver(); + } + for (const section of this._observableSections.values()) { + this._observer.observe(section); + } + } + dispose() { + this._observer.disconnect(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case + config.target = getElement(config.target) || document.body; + + // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only + config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); + } + return config; + } + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; + } + + // unregister any previous listeners + EventHandler.off(this._config.target, EVENT_CLICK); + EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { + const observableSection = this._observableSections.get(event.target.hash); + if (observableSection) { + event.preventDefault(); + const root = this._rootElement || window; + const height = observableSection.offsetTop - this._element.offsetTop; + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + return; + } + + // Chrome 60 doesn't support `scrollTo` + root.scrollTop = height; + } + }); + } + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin + }; + return new IntersectionObserver(entries => this._observerCallback(entries), options); + } + + // The logic of selection + _observerCallback(entries) { + const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); + const activate = entry => { + this._previousScrollData.visibleEntryTop = entry.target.offsetTop; + this._process(targetElement(entry)); + }; + const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; + const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; + this._previousScrollData.parentScrollTop = parentScrollTop; + for (const entry of entries) { + if (!entry.isIntersecting) { + this._activeTarget = null; + this._clearActiveClass(targetElement(entry)); + continue; + } + const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; + // if we are scrolling down, pick the bigger offsetTop + if (userScrollsDown && entryIsLowerThanPrevious) { + activate(entry); + // if parent isn't scrolled, let's keep the first visible item, breaking the iteration + if (!parentScrollTop) { + return; + } + continue; + } + + // if we are scrolling up, pick the smallest offsetTop + if (!userScrollsDown && !entryIsLowerThanPrevious) { + activate(entry); + } + } + } + _initializeTargetsAndObservables() { + this._targetLinks = new Map(); + this._observableSections = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + for (const anchor of targetLinks) { + // ensure that the anchor has an id and is not disabled + if (!anchor.hash || isDisabled(anchor)) { + continue; + } + const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element); + + // ensure that the observableSection exists & is visible + if (isVisible(observableSection)) { + this._targetLinks.set(decodeURI(anchor.hash), anchor); + this._observableSections.set(anchor.hash, observableSection); + } + } + } + _process(target) { + if (this._activeTarget === target) { + return; + } + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE$1); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate dropdown parents + if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { + SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1); + return; + } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both